Initial commit
This commit is contained in:
commit
02a6dabcfb
62 files changed
+15751
No files matched your search
@@ -0,0 +1,88 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetAccountSettings gets account settings (current user's settings from /account/settings)
|
||||
func (c *Client) GetAccountSettings(ctx context.Context) (*models.UserSettings, error) {
|
||||
var result models.UserSettings
|
||||
if err := c.request.Get(ctx, "/account/settings", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateAccountSettings updates account settings
|
||||
func (c *Client) UpdateAccountSettings(ctx context.Context, settings *models.AccountSettings) (*models.AccountSettings, error) {
|
||||
var result models.AccountSettings
|
||||
if err := c.request.Put(ctx, "/account/settings", settings, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetSubscription gets user subscription info
|
||||
func (c *Client) GetSubscription(ctx context.Context) (*models.Subscription, error) {
|
||||
var result models.Subscription
|
||||
if err := c.request.Get(ctx, "/account/subscription", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPlusInfo gets Plus subscription info
|
||||
func (c *Client) GetPlusInfo(ctx context.Context) (*models.PlusInfo, error) {
|
||||
var result models.PlusInfo
|
||||
if err := c.request.Get(ctx, "/account/plus", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPromoCodeStatus gets promo code status
|
||||
func (c *Client) GetPromoCodeStatus(ctx context.Context, code string) (*models.PromoCodeStatus, error) {
|
||||
params := map[string]string{
|
||||
"code": code,
|
||||
}
|
||||
var result models.PromoCodeStatus
|
||||
if err := c.request.Get(ctx, "/promo-code-status", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ActivatePromoCode activates a promo code
|
||||
func (c *Client) ActivatePromoCode(ctx context.Context, code string) (*models.PromoCodeStatus, error) {
|
||||
params := map[string]string{
|
||||
"code": code,
|
||||
}
|
||||
var result models.PromoCodeStatus
|
||||
if err := c.request.Post(ctx, "/promo-code/activate", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetUserSettings gets user settings
|
||||
func (c *Client) GetUserSettings(ctx context.Context, userID string) (*models.UserSettings, error) {
|
||||
params := map[string]string{
|
||||
"user_id": userID,
|
||||
}
|
||||
var result models.UserSettings
|
||||
if err := c.request.Get(ctx, "/user/settings", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetAccountStatus fetches account status
|
||||
func (c *Client) GetAccountStatus(ctx context.Context) (*Status, error) {
|
||||
var result Status
|
||||
if err := c.request.Get(ctx, "/account/status", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetAlbums gets albums by IDs
|
||||
func (c *Client) GetAlbums(ctx context.Context, albumIDs []string) ([]*models.Album, error) {
|
||||
if len(albumIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"album-ids": joinIDs(albumIDs),
|
||||
}
|
||||
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// GetAlbum gets a single album by ID
|
||||
func (c *Client) GetAlbum(ctx context.Context, albumID string) (*models.Album, error) {
|
||||
albums, err := c.GetAlbums(ctx, []string{albumID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(albums) == 0 {
|
||||
return nil, utils.NewYandexMusicError("album not found", utils.ErrNotFound)
|
||||
}
|
||||
return albums[0], nil
|
||||
}
|
||||
|
||||
// GetAlbumsWithTracks gets albums with tracks expanded
|
||||
func (c *Client) GetAlbumsWithTracks(ctx context.Context, albumIDs []string) ([]*models.Album, error) {
|
||||
if len(albumIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"album-ids": joinIDs(albumIDs),
|
||||
"with-tracks": "true",
|
||||
}
|
||||
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetArtists gets artists by IDs
|
||||
func (c *Client) GetArtists(ctx context.Context, artistIDs []string) ([]*models.Artist, error) {
|
||||
if len(artistIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"artist-ids": joinIDs(artistIDs),
|
||||
}
|
||||
|
||||
var result []models.Artist
|
||||
if err := c.request.Get(ctx, "/artists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artists := make([]*models.Artist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
artists[i] = &result[i]
|
||||
}
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
// GetArtist gets a single artist by ID
|
||||
func (c *Client) GetArtist(ctx context.Context, artistID string) (*models.Artist, error) {
|
||||
artists, err := c.GetArtists(ctx, []string{artistID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(artists) == 0 {
|
||||
return nil, utils.NewYandexMusicError("artist not found", utils.ErrNotFound)
|
||||
}
|
||||
return artists[0], nil
|
||||
}
|
||||
|
||||
// GetArtistAlbums gets albums by an artist
|
||||
func (c *Client) GetArtistAlbums(ctx context.Context, artistID string, page, pageSize int) ([]*models.Album, error) {
|
||||
params := map[string]string{
|
||||
"artist-id": artistID,
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page-size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/artists/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// GetArtistTracks gets tracks by an artist
|
||||
func (c *Client) GetArtistTracks(ctx context.Context, artistID string, page, pageSize int) ([]*models.Track, error) {
|
||||
params := map[string]string{
|
||||
"artist-id": artistID,
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page-size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var result []models.Track
|
||||
if err := c.request.Get(ctx, "/artists/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]*models.Track, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tracks[i] = &result[i]
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// Need to add fmt import
|
||||
// "fmt"
|
||||
@@ -0,0 +1,198 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// DeviceCode represents the device authorization response
|
||||
type DeviceCode struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
UserCode string `json:"user_code"`
|
||||
VerificationURL string `json:"verification_url"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
|
||||
// OAuthToken represents an OAuth token response
|
||||
type OAuthToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
// DeviceAuth performs OAuth Device Flow authentication
|
||||
func (r *Request) DeviceAuth(ctx context.Context, onCode func(*DeviceCode)) (*OAuthToken, error) {
|
||||
// Step 1: Request device code
|
||||
deviceCode, err := r.requestDeviceCode(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Call callback with device code
|
||||
if onCode != nil {
|
||||
onCode(deviceCode)
|
||||
}
|
||||
|
||||
// Step 2: Poll for token
|
||||
return r.pollForToken(ctx, deviceCode)
|
||||
}
|
||||
|
||||
// requestDeviceCode requests a device code from Yandex OAuth
|
||||
func (r *Request) requestDeviceCode(ctx context.Context) (*DeviceCode, error) {
|
||||
// Use Yandex OAuth endpoint for device auth
|
||||
data := url.Values{}
|
||||
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
|
||||
data.Set("scope", "login:info login:email yandex_music")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/device/code", strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to create device code request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to request device code", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to read device code response", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, utils.NewYandexMusicError(fmt.Sprintf("device code request failed: %s", string(body)), utils.ErrBadRequest)
|
||||
}
|
||||
|
||||
var deviceCode DeviceCode
|
||||
if err := json.Unmarshal(body, &deviceCode); err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to parse device code", err)
|
||||
}
|
||||
|
||||
return &deviceCode, nil
|
||||
}
|
||||
|
||||
// pollForToken polls for the OAuth token
|
||||
func (r *Request) pollForToken(ctx context.Context, deviceCode *DeviceCode) (*OAuthToken, error) {
|
||||
interval := deviceCode.Interval
|
||||
if interval == 0 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second)
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
|
||||
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
|
||||
data.Set("device_code", deviceCode.DeviceCode)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
if time.Now().After(deadline) {
|
||||
return nil, utils.NewYandexMusicError("device authorization timed out", utils.ErrTimeout)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to create token request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
continue // Retry on network error
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var token OAuthToken
|
||||
if err := json.Unmarshal(body, &token); err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to parse token", err)
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// Check for specific errors
|
||||
var errResp map[string]string
|
||||
json.Unmarshal(body, &errResp)
|
||||
if errResp["error"] == "authorization_pending" {
|
||||
continue // Keep polling
|
||||
}
|
||||
if errResp["error"] == "slow_down" {
|
||||
interval *= 2
|
||||
ticker.Reset(time.Duration(interval) * time.Second)
|
||||
continue
|
||||
}
|
||||
if errResp["error"] == "expired_token" {
|
||||
return nil, utils.NewYandexMusicError("device code expired", utils.ErrTimeout)
|
||||
}
|
||||
if errResp["error"] == "access_denied" {
|
||||
return nil, utils.NewYandexMusicError("access denied by user", utils.ErrUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshToken refreshes an access token using a refresh token
|
||||
func (r *Request) RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error) {
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "refresh_token")
|
||||
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
|
||||
data.Set("refresh_token", refreshToken)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to create refresh request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to refresh token", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to read refresh response", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, utils.NewYandexMusicError(fmt.Sprintf("token refresh failed: %s", string(body)), utils.ErrUnauthorized)
|
||||
}
|
||||
|
||||
var token OAuthToken
|
||||
if err := json.Unmarshal(body, &token); err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to parse refreshed token", err)
|
||||
}
|
||||
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// Missing imports - need to add at top of file
|
||||
// "io"
|
||||
// "net/http"
|
||||
@@ -0,0 +1,154 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// ClientConfig holds configuration for the Client
|
||||
type ClientConfig struct {
|
||||
Token string
|
||||
BaseURL string
|
||||
ProxyURL string
|
||||
Timeout int
|
||||
Language string
|
||||
ReportUnknownFields bool
|
||||
Strict bool
|
||||
OnSchemaMismatch utils.SchemaMismatchHandler
|
||||
JSONBackend utils.JSONBackend
|
||||
}
|
||||
|
||||
// Client is the main Yandex Music API client
|
||||
type Client struct {
|
||||
config ClientConfig
|
||||
request *Request
|
||||
device string
|
||||
me *Status
|
||||
accountUID *int64
|
||||
}
|
||||
|
||||
// NewClient creates a new Yandex Music client
|
||||
func NewClient(config ClientConfig) *Client {
|
||||
if config.BaseURL == "" {
|
||||
config.BaseURL = DefaultBaseURL
|
||||
}
|
||||
if config.Language == "" {
|
||||
config.Language = "ru"
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = int(DefaultTimeout.Seconds())
|
||||
}
|
||||
|
||||
device := fmt.Sprintf("os=Go; os_version=; manufacturer=Mrixs; model=Yandex Music API Go; clid=; device_id=go-client; uuid=go-client")
|
||||
|
||||
reqConfig := RequestConfig{
|
||||
BaseURL: config.BaseURL,
|
||||
Token: config.Token,
|
||||
ProxyURL: config.ProxyURL,
|
||||
Timeout: time.Duration(config.Timeout) * time.Second,
|
||||
Language: config.Language,
|
||||
JSONBackend: config.JSONBackend,
|
||||
ReportUnknownFields: config.ReportUnknownFields,
|
||||
Strict: config.Strict,
|
||||
OnSchemaMismatch: config.OnSchemaMismatch,
|
||||
}
|
||||
|
||||
request := NewRequest(reqConfig)
|
||||
|
||||
return &Client{
|
||||
config: config,
|
||||
request: request,
|
||||
device: device,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the client by fetching account info
|
||||
func (c *Client) Init(ctx context.Context) error {
|
||||
status, err := c.GetAccountStatus(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.me = status
|
||||
if status.Account != nil {
|
||||
c.accountUID = &status.Account.UID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRequest returns the underlying request handler
|
||||
func (c *Client) GetRequest() models.RequestInterface {
|
||||
return c.request
|
||||
}
|
||||
|
||||
// GetToken returns the current token
|
||||
func (c *Client) GetToken() string {
|
||||
return c.config.Token
|
||||
}
|
||||
|
||||
// SetToken updates the token
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.config.Token = token
|
||||
c.request.SetToken(token)
|
||||
}
|
||||
|
||||
// GetDevice returns the device string
|
||||
func (c *Client) GetDevice() string {
|
||||
return c.device
|
||||
}
|
||||
|
||||
// GetMe returns the account status
|
||||
func (c *Client) GetMe() *Status {
|
||||
return c.me
|
||||
}
|
||||
|
||||
// GetAccountUID returns the account UID
|
||||
func (c *Client) GetAccountUID() *int64 {
|
||||
return c.accountUID
|
||||
}
|
||||
|
||||
// Context returns a context for requests (can be overridden for timeouts)
|
||||
func (c *Client) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
// WithContext returns a new client with a custom context
|
||||
func (c *Client) WithContext(ctx context.Context) *Client {
|
||||
// In Go, we typically pass context to each method rather than storing it
|
||||
return c
|
||||
}
|
||||
|
||||
// DeviceAuth performs OAuth Device Flow authentication
|
||||
func (c *Client) DeviceAuth(ctx context.Context, onCode func(*DeviceCode)) (*OAuthToken, error) {
|
||||
return c.request.DeviceAuth(ctx, onCode)
|
||||
}
|
||||
|
||||
// RefreshToken refreshes the access token
|
||||
func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error) {
|
||||
return c.request.RefreshToken(ctx, refreshToken)
|
||||
}
|
||||
|
||||
// Status represents account status
|
||||
type Status struct {
|
||||
Account *Account `json:"account"`
|
||||
// Other fields...
|
||||
}
|
||||
|
||||
// Account represents a user account
|
||||
type Account struct {
|
||||
UID int64 `json:"uid"`
|
||||
Login string `json:"login"`
|
||||
DisplayName string `json:"display_name"`
|
||||
// Other fields...
|
||||
}
|
||||
|
||||
// Track represents a music track (minimal for now)
|
||||
type Track struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Duration int `json:"duration_ms"`
|
||||
// Other fields...
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetClips gets clips
|
||||
func (c *Client) GetClips(ctx context.Context, page, pageSize int) ([]*models.Clip, error) {
|
||||
params := map[string]string{
|
||||
"page": "1",
|
||||
"page_size": "20",
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result []models.Clip
|
||||
if err := c.request.Get(ctx, "/clips", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clips := make([]*models.Clip, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
clips[i] = &result[i]
|
||||
}
|
||||
return clips, nil
|
||||
}
|
||||
|
||||
// GetClipsWillLike gets clips the user might like
|
||||
func (c *Client) GetClipsWillLike(ctx context.Context) (*models.ClipsWillLike, error) {
|
||||
var result models.ClipsWillLike
|
||||
if err := c.request.Get(ctx, "/clips/will-like", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetArtistClips gets clips by an artist
|
||||
func (c *Client) GetArtistClips(ctx context.Context, artistID string) (*models.ArtistClipData, error) {
|
||||
params := map[string]string{
|
||||
"artist_id": artistID,
|
||||
}
|
||||
|
||||
var result models.ArtistClipData
|
||||
if err := c.request.Get(ctx, "/artists/clips", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetShots gets shots
|
||||
func (c *Client) GetShots(ctx context.Context, page, pageSize int) ([]*models.Shot, error) {
|
||||
params := map[string]string{
|
||||
"page": "1",
|
||||
"page_size": "20",
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result []models.Shot
|
||||
if err := c.request.Get(ctx, "/shots", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shots := make([]*models.Shot, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
shots[i] = &result[i]
|
||||
}
|
||||
return shots, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetConcerts gets concerts
|
||||
func (c *Client) GetConcerts(ctx context.Context, page, pageSize int) (*models.ConcertFeed, error) {
|
||||
params := map[string]string{
|
||||
"page": "1",
|
||||
"page_size": "20",
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.ConcertFeed
|
||||
if err := c.request.Get(ctx, "/concerts", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertFeed gets concert feed
|
||||
func (c *Client) GetConcertFeed(ctx context.Context) (*models.ConcertFeed, error) {
|
||||
var result models.ConcertFeed
|
||||
if err := c.request.Get(ctx, "/concerts/feed", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertInfo gets concert info
|
||||
func (c *Client) GetConcertInfo(ctx context.Context, concertID string) (*models.ConcertInfo, error) {
|
||||
params := map[string]string{
|
||||
"concert_id": concertID,
|
||||
}
|
||||
|
||||
var result models.ConcertInfo
|
||||
if err := c.request.Get(ctx, "/concerts/info", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertLocations gets concert locations
|
||||
func (c *Client) GetConcertLocations(ctx context.Context) (*models.ConcertLocations, error) {
|
||||
var result models.ConcertLocations
|
||||
if err := c.request.Get(ctx, "/concerts/locations", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetConcertTabs gets concert tabs
|
||||
func (c *Client) GetConcertTabs(ctx context.Context, concertID string) ([]*models.ConcertTabConfig, error) {
|
||||
params := map[string]string{
|
||||
"concert_id": concertID,
|
||||
}
|
||||
|
||||
var result []models.ConcertTabConfig
|
||||
if err := c.request.Get(ctx, "/concerts/tabs", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tabs := make([]*models.ConcertTabConfig, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tabs[i] = &result[i]
|
||||
}
|
||||
return tabs, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetFeed gets the main feed
|
||||
func (c *Client) GetFeed(ctx context.Context) (*models.Feed, error) {
|
||||
var result models.Feed
|
||||
if err := c.request.Get(ctx, "/feed", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetFeedBlocks gets feed blocks
|
||||
func (c *Client) GetFeedBlocks(ctx context.Context) ([]*models.DayBlock, error) {
|
||||
var result []models.DayBlock
|
||||
if err := c.request.Get(ctx, "/feed/blocks", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blocks := make([]*models.DayBlock, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
blocks[i] = &result[i]
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// GetFeedGeneratedPlaylist gets a generated playlist by ID from feed
|
||||
func (c *Client) GetFeedGeneratedPlaylist(ctx context.Context, playlistID string) (*models.GeneratedPlaylist, error) {
|
||||
params := map[string]string{
|
||||
"playlist_id": playlistID,
|
||||
}
|
||||
|
||||
var result models.GeneratedPlaylist
|
||||
if err := c.request.Get(ctx, "/feed/generated-playlist", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetFeedDay gets a specific day from the feed
|
||||
func (c *Client) GetFeedDay(ctx context.Context, date string) (*models.Day, error) {
|
||||
params := map[string]string{
|
||||
"date": date,
|
||||
}
|
||||
|
||||
var result models.Day
|
||||
if err := c.request.Get(ctx, "/feed/day", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetGeneratedPlaylists gets generated playlists
|
||||
func (c *Client) GetGeneratedPlaylists(ctx context.Context) ([]*models.GeneratedPlaylist, error) {
|
||||
var result []models.GeneratedPlaylist
|
||||
if err := c.request.Get(ctx, "/generated-playlists", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.GeneratedPlaylist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
// GetLanding gets a landing page
|
||||
func (c *Client) GetLanding(ctx context.Context, landingID string) (*models.Landing, error) {
|
||||
params := map[string]string{
|
||||
"landing_id": landingID,
|
||||
}
|
||||
|
||||
var result models.Landing
|
||||
if err := c.request.Get(ctx, "/landing", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLandingList gets a list of landings
|
||||
func (c *Client) GetLandingList(ctx context.Context) (*models.LandingList, error) {
|
||||
var result models.LandingList
|
||||
if err := c.request.Get(ctx, "/landing/list", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLandingBlocks gets landing blocks by block IDs
|
||||
func (c *Client) GetLandingBlocks(ctx context.Context, blockIDs []string) ([]*models.LandingBlock, error) {
|
||||
params := map[string]string{
|
||||
"blocks": joinStrings(blockIDs, ","),
|
||||
}
|
||||
|
||||
var result []models.LandingBlock
|
||||
if err := c.request.Get(ctx, "/landing/blocks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blocks := make([]*models.LandingBlock, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
blocks[i] = &result[i]
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// joinStrings joins a slice of strings with a separator
|
||||
func joinStrings(slice []string, sep string) string {
|
||||
if len(slice) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := slice[0]
|
||||
for _, s := range slice[1:] {
|
||||
result += sep + s
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetMusicHistory gets music history
|
||||
func (c *Client) GetMusicHistory(ctx context.Context) (*models.MusicHistory, error) {
|
||||
var result models.MusicHistory
|
||||
if err := c.request.Get(ctx, "/music-history", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMusicHistoryTabs gets music history tabs
|
||||
func (c *Client) GetMusicHistoryTabs(ctx context.Context) ([]*models.MusicHistoryTab, error) {
|
||||
var result []models.MusicHistoryTab
|
||||
if err := c.request.Get(ctx, "/music-history/tabs", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tabs := make([]*models.MusicHistoryTab, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tabs[i] = &result[i]
|
||||
}
|
||||
return tabs, nil
|
||||
}
|
||||
|
||||
// GetMusicHistoryItems gets music history items
|
||||
func (c *Client) GetMusicHistoryItems(ctx context.Context, tabID string, page, pageSize int) (*models.MusicHistoryItems, error) {
|
||||
params := map[string]string{}
|
||||
if tabID != "" {
|
||||
params["tab_id"] = tabID
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MusicHistoryItems
|
||||
if err := c.request.Get(ctx, "/music-history/items", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetLabels gets all labels
|
||||
func (c *Client) GetLabels(ctx context.Context) ([]*models.Label, error) {
|
||||
var result []models.Label
|
||||
if err := c.request.Get(ctx, "/labels", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labels := make([]*models.Label, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
labels[i] = &result[i]
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// GetLabel gets a label by ID
|
||||
func (c *Client) GetLabel(ctx context.Context, labelID string) (*models.Label, error) {
|
||||
params := map[string]string{
|
||||
"label_id": labelID,
|
||||
}
|
||||
|
||||
var result models.Label
|
||||
if err := c.request.Get(ctx, "/labels", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLabelAlbums gets albums for a label
|
||||
func (c *Client) GetLabelAlbums(ctx context.Context, labelID string, page, pageSize int) (*models.LabelAlbums, error) {
|
||||
params := map[string]string{
|
||||
"label_id": labelID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.LabelAlbums
|
||||
if err := c.request.Get(ctx, "/labels/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLabelArtists gets artists for a label
|
||||
func (c *Client) GetLabelArtists(ctx context.Context, labelID string, page, pageSize int) (*models.LabelArtists, error) {
|
||||
params := map[string]string{
|
||||
"label_id": labelID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.LabelArtists
|
||||
if err := c.request.Get(ctx, "/labels/artists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// LikeTrack likes a track
|
||||
func (c *Client) LikeTrack(ctx context.Context, trackID string) error {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/tracks/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikeTrack unlikes a track
|
||||
func (c *Client) UnlikeTrack(ctx context.Context, trackID string) error {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/tracks/remove", params, &result)
|
||||
}
|
||||
|
||||
// LikeAlbum likes an album
|
||||
func (c *Client) LikeAlbum(ctx context.Context, albumID string) error {
|
||||
params := map[string]string{
|
||||
"album_id": albumID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/albums/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikeAlbum unlikes an album
|
||||
func (c *Client) UnlikeAlbum(ctx context.Context, albumID string) error {
|
||||
params := map[string]string{
|
||||
"album_id": albumID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/albums/remove", params, &result)
|
||||
}
|
||||
|
||||
// LikeArtist likes an artist
|
||||
func (c *Client) LikeArtist(ctx context.Context, artistID string) error {
|
||||
params := map[string]string{
|
||||
"artist_id": artistID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/artists/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikeArtist unlikes an artist
|
||||
func (c *Client) UnlikeArtist(ctx context.Context, artistID string) error {
|
||||
params := map[string]string{
|
||||
"artist_id": artistID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/artists/remove", params, &result)
|
||||
}
|
||||
|
||||
// LikePlaylist likes a playlist
|
||||
func (c *Client) LikePlaylist(ctx context.Context, kind int) error {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/playlists/add", params, &result)
|
||||
}
|
||||
|
||||
// UnlikePlaylist unlikes a playlist
|
||||
func (c *Client) UnlikePlaylist(ctx context.Context, kind int) error {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/users/likes/playlists/remove", params, &result)
|
||||
}
|
||||
|
||||
// GetLikedTracks gets user's liked tracks
|
||||
func (c *Client) GetLikedTracks(ctx context.Context) ([]*models.Track, error) {
|
||||
var result []models.Track
|
||||
if err := c.request.Get(ctx, "/users/likes/tracks", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]*models.Track, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tracks[i] = &result[i]
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// GetLikedAlbums gets user's liked albums
|
||||
func (c *Client) GetLikedAlbums(ctx context.Context) ([]*models.Album, error) {
|
||||
var result []models.Album
|
||||
if err := c.request.Get(ctx, "/users/likes/albums", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albums := make([]*models.Album, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
albums[i] = &result[i]
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
// GetLikedArtists gets user's liked artists
|
||||
func (c *Client) GetLikedArtists(ctx context.Context) ([]*models.Artist, error) {
|
||||
var result []models.Artist
|
||||
if err := c.request.Get(ctx, "/users/likes/artists", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artists := make([]*models.Artist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
artists[i] = &result[i]
|
||||
}
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
// GetLikedPlaylists gets user's liked playlists
|
||||
func (c *Client) GetLikedPlaylists(ctx context.Context) ([]*models.Playlist, error) {
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/users/likes/playlists", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetMetatags gets all metatags
|
||||
func (c *Client) GetMetatags(ctx context.Context) ([]*models.Metatag, error) {
|
||||
var result []models.Metatag
|
||||
if err := c.request.Get(ctx, "/metatags", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metatags := make([]*models.Metatag, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
metatags[i] = &result[i]
|
||||
}
|
||||
return metatags, nil
|
||||
}
|
||||
|
||||
// GetMetatagTree gets metatag tree
|
||||
func (c *Client) GetMetatagTree(ctx context.Context) (*models.MetatagTree, error) {
|
||||
var result models.MetatagTree
|
||||
if err := c.request.Get(ctx, "/metatags/tree", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMetatagArtists gets artists for a metatag
|
||||
func (c *Client) GetMetatagArtists(ctx context.Context, metatagID string, page, pageSize int) (*models.MetatagArtists, error) {
|
||||
params := map[string]string{
|
||||
"metatag_id": metatagID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MetatagArtists
|
||||
if err := c.request.Get(ctx, "/metatags/artists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMetatagAlbums gets albums for a metatag
|
||||
func (c *Client) GetMetatagAlbums(ctx context.Context, metatagID string, page, pageSize int) (*models.MetatagAlbums, error) {
|
||||
params := map[string]string{
|
||||
"metatag_id": metatagID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MetatagAlbums
|
||||
if err := c.request.Get(ctx, "/metatags/albums", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetMetatagPlaylists gets playlists for a metatag
|
||||
func (c *Client) GetMetatagPlaylists(ctx context.Context, metatagID string, page, pageSize int) (*models.MetatagPlaylists, error) {
|
||||
params := map[string]string{
|
||||
"metatag_id": metatagID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.MetatagPlaylists
|
||||
if err := c.request.Get(ctx, "/metatags/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetGenres gets all genres
|
||||
func (c *Client) GetGenres(ctx context.Context) ([]*models.Genre, error) {
|
||||
var result []models.Genre
|
||||
if err := c.request.Get(ctx, "/genres", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
genres := make([]*models.Genre, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
genres[i] = &result[i]
|
||||
}
|
||||
return genres, nil
|
||||
}
|
||||
|
||||
// GetCharts gets charts
|
||||
func (c *Client) GetCharts(ctx context.Context) ([]*models.Chart, error) {
|
||||
var result []models.Chart
|
||||
if err := c.request.Get(ctx, "/charts", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
charts := make([]*models.Chart, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
charts[i] = &result[i]
|
||||
}
|
||||
return charts, nil
|
||||
}
|
||||
|
||||
// GetChartInfo gets chart info
|
||||
func (c *Client) GetChartInfo(ctx context.Context, chartID string) (*models.ChartInfo, error) {
|
||||
params := map[string]string{
|
||||
"chart_id": chartID,
|
||||
}
|
||||
|
||||
var result models.ChartInfo
|
||||
if err := c.request.Get(ctx, "/charts/info", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetChartItems gets chart items
|
||||
func (c *Client) GetChartItems(ctx context.Context, chartID string, page, pageSize int) (*models.ChartItems, error) {
|
||||
params := map[string]string{
|
||||
"chart_id": chartID,
|
||||
}
|
||||
if page > 0 {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
params["page_size"] = fmt.Sprintf("%d", pageSize)
|
||||
}
|
||||
|
||||
var result models.ChartItems
|
||||
if err := c.request.Get(ctx, "/charts/items", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetChartMenu gets chart menu
|
||||
func (c *Client) GetChartMenu(ctx context.Context, chartID string) (*models.ChartInfoMenu, error) {
|
||||
params := map[string]string{
|
||||
"chart_id": chartID,
|
||||
}
|
||||
|
||||
var result models.ChartInfoMenu
|
||||
if err := c.request.Get(ctx, "/charts/menu", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetPins gets user's pins
|
||||
func (c *Client) GetPins(ctx context.Context) (*models.PinsList, error) {
|
||||
var result models.PinsList
|
||||
if err := c.request.Get(ctx, "/pins", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// AddPin adds a pin
|
||||
func (c *Client) AddPin(ctx context.Context, pinType, entityID string) (*models.Pin, error) {
|
||||
params := map[string]string{
|
||||
"type": pinType,
|
||||
"entity_id": entityID,
|
||||
}
|
||||
|
||||
var result models.Pin
|
||||
if err := c.request.Post(ctx, "/pins", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RemovePin removes a pin
|
||||
func (c *Client) RemovePin(ctx context.Context, pinID string) error {
|
||||
params := map[string]string{
|
||||
"pin_id": pinID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Delete(ctx, "/pins", params, &result)
|
||||
}
|
||||
|
||||
// GetPresaves gets user's presaves
|
||||
func (c *Client) GetPresaves(ctx context.Context) (*models.Presaves, error) {
|
||||
var result models.Presaves
|
||||
if err := c.request.Get(ctx, "/presaves", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// playlistResponse creates a JSON response for a playlist list
|
||||
func playlistResponse(playlists []map[string]any) []byte {
|
||||
resp := map[string]any{
|
||||
"result": playlists,
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return data
|
||||
}
|
||||
|
||||
// singlePlaylistResponse creates a JSON response for a single playlist
|
||||
func singlePlaylistResponse(playlist map[string]any) []byte {
|
||||
resp := map[string]any{
|
||||
"result": playlist,
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return data
|
||||
}
|
||||
|
||||
// errorResponse creates an error JSON response
|
||||
func errorResponse(errMsg string) []byte {
|
||||
resp := map[string]any{
|
||||
"error": errMsg,
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return data
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylists(t *testing.T) {
|
||||
// Sample playlist data
|
||||
playlists := []map[string]any{
|
||||
{
|
||||
"kind": 1,
|
||||
"title": "Playlist 1",
|
||||
"description": "First playlist",
|
||||
"cover_uri": "https://example.com/cover1.jpg",
|
||||
"created": "2023-01-01T00:00:00+03:00",
|
||||
"modified": "2023-01-02T00:00:00+03:00",
|
||||
"track_count": 10,
|
||||
"duration_ms": 1800000,
|
||||
"likes_count": 5,
|
||||
"is_public": true,
|
||||
"revision": 1,
|
||||
"uuid": "uuid-1",
|
||||
"tags": []string{"rock", "pop"},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "rock",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-1",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "user1",
|
||||
"display_name": "User One",
|
||||
"cover_uri": "https://example.com/user1.jpg",
|
||||
"sex": "m",
|
||||
"public_playlists": 10,
|
||||
"followers_count": 100,
|
||||
"followings_count": 50,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"title": "Playlist 2",
|
||||
"description": "Second playlist",
|
||||
"cover_uri": "https://example.com/cover2.jpg",
|
||||
"created": "2023-02-01T00:00:00+03:00",
|
||||
"modified": "2023-02-02T00:00:00+03:00",
|
||||
"track_count": 5,
|
||||
"duration_ms": 900000,
|
||||
"likes_count": 3,
|
||||
"is_public": false,
|
||||
"revision": 2,
|
||||
"uuid": "uuid-2",
|
||||
"tags": []string{"jazz"},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "jazz",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-2",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "user1",
|
||||
"display_name": "User One",
|
||||
"cover_uri": "https://example.com/user1.jpg",
|
||||
"sex": "m",
|
||||
"public_playlists": 10,
|
||||
"followers_count": 100,
|
||||
"followings_count": 50,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
},
|
||||
}
|
||||
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/playlists" {
|
||||
t.Errorf("Expected path /playlists, got %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("kinds") != "1,2" {
|
||||
t.Errorf("Expected kinds=1,2, got %s", r.URL.Query().Get("kinds"))
|
||||
}
|
||||
if r.Header.Get("Authorization") != "OAuth test-token" {
|
||||
t.Errorf("Expected Authorization header, got %s", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(playlistResponse(playlists))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylists(context.Background(), []int{1, 2})
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlaylists failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("Expected 2 playlists, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Kind != 1 {
|
||||
t.Errorf("Expected first playlist kind 1, got %d", result[0].Kind)
|
||||
}
|
||||
if result[0].Title != "Playlist 1" {
|
||||
t.Errorf("Expected first playlist title 'Playlist 1', got '%s'", result[0].Title)
|
||||
}
|
||||
if result[0].Description != "First playlist" {
|
||||
t.Errorf("Expected first playlist description 'First playlist', got '%s'", result[0].Description)
|
||||
}
|
||||
if result[0].TrackCount != 10 {
|
||||
t.Errorf("Expected first playlist track_count 10, got %d", result[0].TrackCount)
|
||||
}
|
||||
if result[0].Owner == nil || result[0].Owner.UID != 12345 {
|
||||
t.Errorf("Expected owner UID 12345")
|
||||
}
|
||||
if result[0].Owner.Login != "user1" {
|
||||
t.Errorf("Expected owner login 'user1', got '%s'", result[0].Owner.Login)
|
||||
}
|
||||
|
||||
if result[1].Kind != 2 {
|
||||
t.Errorf("Expected second playlist kind 2, got %d", result[1].Kind)
|
||||
}
|
||||
if result[1].Title != "Playlist 2" {
|
||||
t.Errorf("Expected second playlist title 'Playlist 2', got '%s'", result[1].Title)
|
||||
}
|
||||
if result[1].IsPublic != false {
|
||||
t.Errorf("Expected second playlist is_public false, got %v", result[1].IsPublic)
|
||||
}
|
||||
|
||||
// Verify client is set on playlists
|
||||
for _, p := range result {
|
||||
if p.GetClient() != client {
|
||||
t.Error("Expected client to be set on playlist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylists_Empty(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("Server should not be called for empty kinds")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylists(context.Background(), []int{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlaylists failed: %v", err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for empty kinds, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylist(t *testing.T) {
|
||||
playlist := map[string]any{
|
||||
"kind": 42,
|
||||
"title": "My Playlist",
|
||||
"description": "A test playlist",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"created": "2023-01-01T00:00:00+03:00",
|
||||
"modified": "2023-01-02T00:00:00+03:00",
|
||||
"track_count": 15,
|
||||
"duration_ms": 2700000,
|
||||
"likes_count": 10,
|
||||
"is_public": true,
|
||||
"revision": 5,
|
||||
"uuid": "uuid-42",
|
||||
"tags": []string{"rock", "metal"},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "rock",
|
||||
"owner_uid": 999,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-42",
|
||||
"owner": map[string]any{
|
||||
"uid": 999,
|
||||
"login": "testuser",
|
||||
"display_name": "Test User",
|
||||
"cover_uri": "https://example.com/user.jpg",
|
||||
"sex": "f",
|
||||
"public_playlists": 5,
|
||||
"followers_count": 200,
|
||||
"followings_count": 100,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
}
|
||||
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/playlists" {
|
||||
t.Errorf("Expected path /playlists, got %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("kinds") != "42" {
|
||||
t.Errorf("Expected kinds=42, got %s", r.URL.Query().Get("kinds"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(playlistResponse([]map[string]any{playlist}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylist(context.Background(), 42)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPlaylist failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected playlist, got nil")
|
||||
}
|
||||
|
||||
if result.Kind != 42 {
|
||||
t.Errorf("Expected kind 42, got %d", result.Kind)
|
||||
}
|
||||
if result.Title != "My Playlist" {
|
||||
t.Errorf("Expected title 'My Playlist', got '%s'", result.Title)
|
||||
}
|
||||
if result.Description != "A test playlist" {
|
||||
t.Errorf("Expected description 'A test playlist', got '%s'", result.Description)
|
||||
}
|
||||
if result.TrackCount != 15 {
|
||||
t.Errorf("Expected track_count 15, got %d", result.TrackCount)
|
||||
}
|
||||
if result.Revision != 5 {
|
||||
t.Errorf("Expected revision 5, got %d", result.Revision)
|
||||
}
|
||||
if result.Owner == nil || result.Owner.Login != "testuser" {
|
||||
t.Errorf("Expected owner login 'testuser'")
|
||||
}
|
||||
if result.GetClient() != client {
|
||||
t.Error("Expected client to be set on playlist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylist_NotFound(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(playlistResponse([]map[string]any{}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.GetPlaylist(context.Background(), 999)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for not found playlist")
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil playlist, got %v", result)
|
||||
}
|
||||
// Check that it's a not found error
|
||||
if err.Error() != "playlist not found" && err.Error() != "playlist not found: not found" {
|
||||
t.Errorf("Expected 'playlist not found' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetPlaylist_Error(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(errorResponse("Internal server error"))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
_, err := client.GetPlaylist(context.Background(), 1)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
if err.Error() == "" {
|
||||
t.Errorf("Expected non-empty error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreatePlaylist(t *testing.T) {
|
||||
createdPlaylist := map[string]any{
|
||||
"kind": 100,
|
||||
"title": "New Playlist",
|
||||
"description": "",
|
||||
"cover_uri": "",
|
||||
"created": "2024-01-01T00:00:00+03:00",
|
||||
"modified": "2024-01-01T00:00:00+03:00",
|
||||
"track_count": 0,
|
||||
"duration_ms": 0,
|
||||
"likes_count": 0,
|
||||
"is_public": true,
|
||||
"revision": 1,
|
||||
"uuid": "new-uuid-100",
|
||||
"tags": []any{},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-new-100",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "me",
|
||||
"display_name": "Me",
|
||||
"cover_uri": "",
|
||||
"sex": "",
|
||||
"public_playlists": 1,
|
||||
"followers_count": 0,
|
||||
"followings_count": 0,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
}
|
||||
|
||||
var capturedBody map[string]any
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("Expected POST method, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/users/me/playlists" {
|
||||
t.Errorf("Expected path /users/me/playlists, got %s", r.URL.Path)
|
||||
}
|
||||
// Read and parse request body
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&capturedBody); err != nil {
|
||||
t.Errorf("Failed to decode request body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(singlePlaylistResponse(createdPlaylist))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.CreatePlaylist(context.Background(), "New Playlist", "public")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePlaylist failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected playlist, got nil")
|
||||
}
|
||||
|
||||
if result.Kind != 100 {
|
||||
t.Errorf("Expected kind 100, got %d", result.Kind)
|
||||
}
|
||||
if result.Title != "New Playlist" {
|
||||
t.Errorf("Expected title 'New Playlist', got '%s'", result.Title)
|
||||
}
|
||||
if result.IsPublic != true {
|
||||
t.Errorf("Expected is_public true, got %v", result.IsPublic)
|
||||
}
|
||||
if result.GetClient() != client {
|
||||
t.Error("Expected client to be set on playlist")
|
||||
}
|
||||
|
||||
// Verify the request body
|
||||
if capturedBody == nil {
|
||||
t.Fatal("Request body not captured")
|
||||
}
|
||||
if capturedBody["title"] != "New Playlist" {
|
||||
t.Errorf("Expected title 'New Playlist' in body, got %v", capturedBody["title"])
|
||||
}
|
||||
if capturedBody["visibility"] != "public" {
|
||||
t.Errorf("Expected visibility 'public' in body, got %v", capturedBody["visibility"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreatePlaylist_Private(t *testing.T) {
|
||||
createdPlaylist := map[string]any{
|
||||
"kind": 101,
|
||||
"title": "Private Playlist",
|
||||
"description": "",
|
||||
"cover_uri": "",
|
||||
"created": "2024-01-01T00:00:00+03:00",
|
||||
"modified": "2024-01-01T00:00:00+03:00",
|
||||
"track_count": 0,
|
||||
"duration_ms": 0,
|
||||
"likes_count": 0,
|
||||
"is_public": false,
|
||||
"revision": 1,
|
||||
"uuid": "new-uuid-101",
|
||||
"tags": []any{},
|
||||
"access_type": "full",
|
||||
"available": true,
|
||||
"content_warning": "",
|
||||
"genre": "",
|
||||
"owner_uid": 12345,
|
||||
"shared": false,
|
||||
"snapshot_id": "snap-new-101",
|
||||
"owner": map[string]any{
|
||||
"uid": 12345,
|
||||
"login": "me",
|
||||
"display_name": "Me",
|
||||
"cover_uri": "",
|
||||
"sex": "",
|
||||
"public_playlists": 1,
|
||||
"followers_count": 0,
|
||||
"followings_count": 0,
|
||||
"has_playlists": true,
|
||||
"available": true,
|
||||
},
|
||||
"tracks": []any{},
|
||||
"similar_playlists": []any{},
|
||||
}
|
||||
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(singlePlaylistResponse(createdPlaylist))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
result, err := client.CreatePlaylist(context.Background(), "Private Playlist", "private")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePlaylist failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected playlist, got nil")
|
||||
}
|
||||
|
||||
if result.Title != "Private Playlist" {
|
||||
t.Errorf("Expected title 'Private Playlist', got '%s'", result.Title)
|
||||
}
|
||||
if result.IsPublic != false {
|
||||
t.Errorf("Expected is_public false for private playlist, got %v", result.IsPublic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CreatePlaylist_Error(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(errorResponse("unauthorized"))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
_, err := client.CreatePlaylist(context.Background(), "Test", "public")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
// Check that it's an unauthorized error
|
||||
if !utils.IsUnauthorizedError(err) {
|
||||
t.Errorf("Expected unauthorized error, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetPlaylists gets playlists by IDs (kinds)
|
||||
func (c *Client) GetPlaylists(ctx context.Context, kinds []int) ([]*models.Playlist, error) {
|
||||
if len(kinds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert kinds to string
|
||||
kindsStr := ""
|
||||
for i, k := range kinds {
|
||||
if i > 0 {
|
||||
kindsStr += ","
|
||||
}
|
||||
kindsStr += fmt.Sprintf("%d", k)
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"kinds": kindsStr,
|
||||
}
|
||||
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
// GetPlaylist gets a single playlist by kind
|
||||
func (c *Client) GetPlaylist(ctx context.Context, kind int) (*models.Playlist, error) {
|
||||
playlists, err := c.GetPlaylists(ctx, []int{kind})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(playlists) == 0 {
|
||||
return nil, utils.NewYandexMusicError("playlist not found", utils.ErrNotFound)
|
||||
}
|
||||
return playlists[0], nil
|
||||
}
|
||||
|
||||
// GetUserPlaylists gets playlists for a user
|
||||
func (c *Client) GetUserPlaylists(ctx context.Context, userID string) ([]*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"user-id": userID,
|
||||
}
|
||||
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/users/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
// CreatePlaylist creates a new playlist
|
||||
func (c *Client) CreatePlaylist(ctx context.Context, title string, visibility string) (*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"title": title,
|
||||
"visibility": visibility, // "public" or "private"
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Post(ctx, "/users/me/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// EditPlaylist edits an existing playlist
|
||||
func (c *Client) EditPlaylist(ctx context.Context, kind int, title, description, visibility string) (*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
if title != "" {
|
||||
params["title"] = title
|
||||
}
|
||||
if description != "" {
|
||||
params["description"] = description
|
||||
}
|
||||
if visibility != "" {
|
||||
params["visibility"] = visibility
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Put(ctx, "/users/me/playlists", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeletePlaylist deletes a playlist
|
||||
func (c *Client) DeletePlaylist(ctx context.Context, kind int) error {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Delete(ctx, "/users/me/playlists", params, &result)
|
||||
}
|
||||
|
||||
// AddTracksToPlaylist adds tracks to a playlist
|
||||
func (c *Client) AddTracksToPlaylist(ctx context.Context, kind int, trackIDs []string, revision int) (*models.Playlist, error) {
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, utils.NewYandexMusicError("no tracks provided", utils.ErrBadRequest)
|
||||
}
|
||||
|
||||
tracksStr := ""
|
||||
for i, id := range trackIDs {
|
||||
if i > 0 {
|
||||
tracksStr += ","
|
||||
}
|
||||
tracksStr += id
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
"track_ids": tracksStr,
|
||||
"revision": fmt.Sprintf("%d", revision),
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Post(ctx, "/users/me/playlists/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RemoveTracksFromPlaylist removes tracks from a playlist
|
||||
func (c *Client) RemoveTracksFromPlaylist(ctx context.Context, kind int, trackIDs []string, revision int) (*models.Playlist, error) {
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, utils.NewYandexMusicError("no tracks provided", utils.ErrBadRequest)
|
||||
}
|
||||
|
||||
tracksStr := ""
|
||||
for i, id := range trackIDs {
|
||||
if i > 0 {
|
||||
tracksStr += ","
|
||||
}
|
||||
tracksStr += id
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
"track_ids": tracksStr,
|
||||
"revision": fmt.Sprintf("%d", revision),
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Delete(ctx, "/users/me/playlists/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ReorderPlaylistTracks reorders tracks in a playlist
|
||||
func (c *Client) ReorderPlaylistTracks(ctx context.Context, kind int, from, to, revision int) (*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
"from": fmt.Sprintf("%d", from),
|
||||
"to": fmt.Sprintf("%d", to),
|
||||
"revision": fmt.Sprintf("%d", revision),
|
||||
}
|
||||
|
||||
var result models.Playlist
|
||||
if err := c.request.Put(ctx, "/users/me/playlists/tracks/reorder", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPlaylistRecommendations gets playlist recommendations
|
||||
func (c *Client) GetPlaylistRecommendations(ctx context.Context, kind int) ([]*models.Playlist, error) {
|
||||
params := map[string]string{
|
||||
"kind": fmt.Sprintf("%d", kind),
|
||||
}
|
||||
|
||||
var result []models.Playlist
|
||||
if err := c.request.Get(ctx, "/playlists/recommendations", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists := make([]*models.Playlist, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
playlists[i] = &result[i]
|
||||
}
|
||||
return playlists, nil
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetQueue gets the current queue
|
||||
func (c *Client) GetQueue(ctx context.Context) (*models.Queue, error) {
|
||||
var result models.Queue
|
||||
if err := c.request.Get(ctx, "/queue", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CreateQueue creates a new queue
|
||||
func (c *Client) CreateQueue(ctx context.Context, trackID string, queueType string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
"queue_type": queueType, // "radio", "album", "playlist", etc.
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetQueueItem gets a queue item
|
||||
func (c *Client) GetQueueItem(ctx context.Context, queueID string) (*models.QueueItem, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.QueueItem
|
||||
if err := c.request.Get(ctx, "/queue/item", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// AddToQueue adds a track to the queue
|
||||
func (c *Client) AddToQueue(ctx context.Context, queueID, trackID string, position int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"track_id": trackID,
|
||||
"position": fmt.Sprintf("%d", position),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/add", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RemoveFromQueue removes a track from the queue
|
||||
func (c *Client) RemoveFromQueue(ctx context.Context, queueID, trackID string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Delete(ctx, "/queue/remove", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ReorderQueue reorders the queue
|
||||
func (c *Client) ReorderQueue(ctx context.Context, queueID string, from, to int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"from": fmt.Sprintf("%d", from),
|
||||
"to": fmt.Sprintf("%d", to),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Put(ctx, "/queue/reorder", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ClearQueue clears the queue
|
||||
func (c *Client) ClearQueue(ctx context.Context, queueID string) error {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
var result map[string]any
|
||||
return c.request.Delete(ctx, "/queue/clear", params, &result)
|
||||
}
|
||||
|
||||
// NextTrack moves to the next track in the queue
|
||||
func (c *Client) NextTrack(ctx context.Context, queueID string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/next", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// PreviousTrack moves to the previous track in the queue
|
||||
func (c *Client) PreviousTrack(ctx context.Context, queueID string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/prev", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Seek seeks to a specific position in the current track (in milliseconds)
|
||||
func (c *Client) Seek(ctx context.Context, queueID string, positionMs int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"position": fmt.Sprintf("%d", positionMs),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/seek", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetRepeatMode sets the repeat mode for the queue
|
||||
// mode can be "off", "one", or "all"
|
||||
func (c *Client) SetRepeatMode(ctx context.Context, queueID, mode string) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/repeat", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetShuffle enables or disables shuffle for the queue
|
||||
func (c *Client) SetShuffle(ctx context.Context, queueID string, enabled bool) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"enabled": fmt.Sprintf("%t", enabled),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/shuffle", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SetVolume sets the volume for the queue (0-100)
|
||||
func (c *Client) SetVolume(ctx context.Context, queueID string, volume int) (*models.Queue, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"volume": fmt.Sprintf("%d", volume),
|
||||
}
|
||||
|
||||
var result models.Queue
|
||||
if err := c.request.Post(ctx, "/queue/volume", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetCurrentTrack gets the currently playing track from the queue
|
||||
func (c *Client) GetCurrentTrack(ctx context.Context, queueID string) (*models.QueueItem, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
}
|
||||
|
||||
var result models.QueueItem
|
||||
if err := c.request.Get(ctx, "/queue/current", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetQueueHistory gets the queue history (recently played tracks)
|
||||
func (c *Client) GetQueueHistory(ctx context.Context, queueID string, limit int) ([]*models.QueueItem, error) {
|
||||
params := map[string]string{
|
||||
"queue_id": queueID,
|
||||
"limit": fmt.Sprintf("%d", limit),
|
||||
}
|
||||
|
||||
var result []models.QueueItem
|
||||
if err := c.request.Get(ctx, "/queue/history", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]*models.QueueItem, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
items[i] = &result[i]
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetPlayContexts gets play contexts
|
||||
func (c *Client) GetPlayContexts(ctx context.Context) ([]*models.PlayContext, error) {
|
||||
var result []models.PlayContext
|
||||
if err := c.request.Get(ctx, "/play-contexts", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contexts := make([]*models.PlayContext, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
contexts[i] = &result[i]
|
||||
}
|
||||
return contexts, nil
|
||||
}
|
||||
|
||||
// GetPlayContextsData gets play contexts data
|
||||
func (c *Client) GetPlayContextsData(ctx context.Context) (*models.PlayContextsData, error) {
|
||||
var result models.PlayContextsData
|
||||
if err := c.request.Get(ctx, "/play-contexts/data", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetRadioStations gets radio stations
|
||||
func (c *Client) GetRadioStations(ctx context.Context) ([]*models.RadioStation, error) {
|
||||
var result []models.RadioStation
|
||||
if err := c.request.Get(ctx, "/radio/stations", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stations := make([]*models.RadioStation, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
stations[i] = &result[i]
|
||||
}
|
||||
return stations, nil
|
||||
}
|
||||
|
||||
// GetRadioStation gets a radio station by ID
|
||||
func (c *Client) GetRadioStation(ctx context.Context, stationID string) (*models.RadioStation, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
|
||||
var result models.RadioStation
|
||||
if err := c.request.Get(ctx, "/radio/station", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetStationTracks gets tracks for a radio station
|
||||
func (c *Client) GetStationTracks(ctx context.Context, stationID string, limit int) (*models.StationTracksResult, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
if limit > 0 {
|
||||
params["limit"] = fmt.Sprintf("%d", limit)
|
||||
}
|
||||
|
||||
var result models.StationTracksResult
|
||||
if err := c.request.Get(ctx, "/radio/station/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetRotorStations gets rotor stations (generative)
|
||||
func (c *Client) GetRotorStations(ctx context.Context) ([]*models.RotorStation, error) {
|
||||
var result []models.RotorStation
|
||||
if err := c.request.Get(ctx, "/rotor/stations", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stations := make([]*models.RotorStation, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
stations[i] = &result[i]
|
||||
}
|
||||
return stations, nil
|
||||
}
|
||||
|
||||
// GetRotorStation gets a rotor station by ID
|
||||
func (c *Client) GetRotorStation(ctx context.Context, stationID string) (*models.RotorStation, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
|
||||
var result models.RotorStation
|
||||
if err := c.request.Get(ctx, "/rotor/station", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetRotorStationTracks gets tracks for a rotor station
|
||||
func (c *Client) GetRotorStationTracks(ctx context.Context, stationID string, limit int) (*models.RotorStationTracksResult, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
if limit > 0 {
|
||||
params["limit"] = fmt.Sprintf("%d", limit)
|
||||
}
|
||||
|
||||
var result models.RotorStationTracksResult
|
||||
if err := c.request.Get(ctx, "/rotor/station/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// StartRotorSession starts a rotor session
|
||||
func (c *Client) StartRotorSession(ctx context.Context, stationID string) (*models.RotorSession, error) {
|
||||
params := map[string]string{
|
||||
"station_id": stationID,
|
||||
}
|
||||
|
||||
var result models.RotorSession
|
||||
if err := c.request.Post(ctx, "/rotor/session/start", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetRotorSession gets rotor session info
|
||||
func (c *Client) GetRotorSession(ctx context.Context, sessionID string) (*models.RotorSession, error) {
|
||||
params := map[string]string{
|
||||
"session_id": sessionID,
|
||||
}
|
||||
|
||||
var result models.RotorSession
|
||||
if err := c.request.Get(ctx, "/rotor/session", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SetClient(c)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RotorSessionFeedback sends feedback for a rotor session track
|
||||
func (c *Client) RotorSessionFeedback(ctx context.Context, sessionID, trackID, feedbackType string) error {
|
||||
params := map[string]string{
|
||||
"session_id": sessionID,
|
||||
"track_id": trackID,
|
||||
"feedback": feedbackType, // "like", "dislike", "skip"
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
return c.request.Post(ctx, "/rotor/session/feedback", params, &result)
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// Default values
|
||||
const (
|
||||
DefaultBaseURL = "https://api.music.yandex.net"
|
||||
DefaultTimeout = 5 * time.Second
|
||||
UserAgent = "Yandex-Music-API"
|
||||
DefaultClientHeader = "YandexMusicAndroid/24023621"
|
||||
)
|
||||
|
||||
// RequestConfig holds configuration for the request client
|
||||
type RequestConfig struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
ProxyURL string
|
||||
Timeout time.Duration
|
||||
Headers map[string]string
|
||||
JSONBackend utils.JSONBackend
|
||||
Language string
|
||||
Device string
|
||||
ReportUnknownFields bool
|
||||
Strict bool
|
||||
OnSchemaMismatch utils.SchemaMismatchHandler
|
||||
}
|
||||
|
||||
// Request handles HTTP requests to Yandex Music API
|
||||
type Request struct {
|
||||
config RequestConfig
|
||||
client *http.Client
|
||||
schemaTracker *utils.SchemaMismatchTracker
|
||||
}
|
||||
|
||||
// NewRequest creates a new Request instance
|
||||
func NewRequest(config RequestConfig) *Request {
|
||||
if config.BaseURL == "" {
|
||||
config.BaseURL = DefaultBaseURL
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = DefaultTimeout
|
||||
}
|
||||
if config.Headers == nil {
|
||||
config.Headers = make(map[string]string)
|
||||
}
|
||||
if config.JSONBackend == nil {
|
||||
config.JSONBackend = utils.GetDefaultJSONBackend()
|
||||
}
|
||||
|
||||
// Set default headers
|
||||
if _, ok := config.Headers["X-Yandex-Music-Client"]; !ok {
|
||||
config.Headers["X-Yandex-Music-Client"] = DefaultClientHeader
|
||||
}
|
||||
|
||||
// Configure HTTP client
|
||||
httpClient := &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
}
|
||||
|
||||
// Configure proxy if provided
|
||||
if config.ProxyURL != "" {
|
||||
proxyURL, err := url.Parse(config.ProxyURL)
|
||||
if err == nil {
|
||||
httpClient.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r := &Request{
|
||||
config: config,
|
||||
client: httpClient,
|
||||
schemaTracker: utils.NewSchemaMismatchTracker(),
|
||||
}
|
||||
|
||||
// Set token if provided
|
||||
if config.Token != "" {
|
||||
r.SetToken(config.Token)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetToken updates the authorization token
|
||||
func (r *Request) SetToken(token string) {
|
||||
r.config.Token = token
|
||||
if token != "" {
|
||||
r.config.Headers["Authorization"] = "OAuth " + token
|
||||
} else {
|
||||
delete(r.config.Headers, "Authorization")
|
||||
}
|
||||
}
|
||||
|
||||
// SetLanguage sets the Accept-Language header
|
||||
func (r *Request) SetLanguage(lang string) {
|
||||
r.config.Language = lang
|
||||
r.config.Headers["Accept-Language"] = lang
|
||||
}
|
||||
|
||||
// Get performs a GET request
|
||||
func (r *Request) Get(ctx context.Context, path string, params map[string]string, result any) error {
|
||||
return r.doRequest(ctx, "GET", path, params, nil, result)
|
||||
}
|
||||
|
||||
// Post performs a POST request
|
||||
func (r *Request) Post(ctx context.Context, path string, body any, result any) error {
|
||||
return r.doRequest(ctx, "POST", path, nil, body, result)
|
||||
}
|
||||
|
||||
// Put performs a PUT request
|
||||
func (r *Request) Put(ctx context.Context, path string, body any, result any) error {
|
||||
return r.doRequest(ctx, "PUT", path, nil, body, result)
|
||||
}
|
||||
|
||||
// Delete performs a DELETE request
|
||||
func (r *Request) Delete(ctx context.Context, path string, body any, result any) error {
|
||||
return r.doRequest(ctx, "DELETE", path, nil, body, result)
|
||||
}
|
||||
|
||||
// doRequest performs the actual HTTP request
|
||||
func (r *Request) doRequest(ctx context.Context, method, path string, params map[string]string, body any, result any) error {
|
||||
// Build URL
|
||||
fullURL := r.config.BaseURL + path
|
||||
if len(params) > 0 {
|
||||
q := url.Values{}
|
||||
for k, v := range params {
|
||||
q.Set(k, v)
|
||||
}
|
||||
fullURL += "?" + q.Encode()
|
||||
}
|
||||
|
||||
// Prepare request body
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
bodyBytes, err := r.config.JSONBackend.Marshal(body)
|
||||
if err != nil {
|
||||
return utils.NewYandexMusicError("failed to marshal request body", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
|
||||
if err != nil {
|
||||
return utils.NewYandexMusicError("failed to create request", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
for k, v := range r.config.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
// Execute request
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "timeout") {
|
||||
return utils.NewYandexMusicError("request timeout", utils.ErrTimeout)
|
||||
}
|
||||
return utils.NewYandexMusicError("network error", utils.ErrNetwork)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return utils.NewYandexMusicError("failed to read response", err)
|
||||
}
|
||||
|
||||
// Handle error status codes
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return r.handleErrorResponse(resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
if result != nil {
|
||||
apiResp, err := utils.DeJSON(respBody)
|
||||
if err != nil {
|
||||
return utils.NewYandexMusicError("failed to parse response", err)
|
||||
}
|
||||
|
||||
if apiResp.HasError() {
|
||||
return utils.NewYandexMusicError(apiResp.GetError(), utils.ErrBadRequest)
|
||||
}
|
||||
|
||||
// Unmarshal result into target
|
||||
if apiResp.Result != nil {
|
||||
if err := r.config.JSONBackend.Unmarshal(apiResp.Result, result); err != nil {
|
||||
return utils.NewYandexMusicError("failed to unmarshal result", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleErrorResponse handles HTTP error responses
|
||||
func (r *Request) handleErrorResponse(statusCode int, body []byte) error {
|
||||
var msg string
|
||||
apiResp, err := utils.DeJSON(body)
|
||||
if err == nil && apiResp != nil {
|
||||
msg = apiResp.GetError()
|
||||
} else {
|
||||
msg = string(body)
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return utils.NewYandexMusicError(msg, utils.ErrUnauthorized)
|
||||
case http.StatusBadRequest:
|
||||
return utils.NewYandexMusicError(msg, utils.ErrBadRequest)
|
||||
case http.StatusNotFound:
|
||||
return utils.NewYandexMusicError(msg, utils.ErrNotFound)
|
||||
case http.StatusConflict, http.StatusRequestEntityTooLarge:
|
||||
return utils.NewYandexMusicError(msg, utils.ErrNetwork)
|
||||
case http.StatusBadGateway:
|
||||
return utils.NewYandexMusicError("Bad Gateway", utils.ErrNetwork)
|
||||
default:
|
||||
return utils.NewYandexMusicError(fmt.Sprintf("%s (%d): %s", msg, statusCode, string(body)), utils.ErrNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
// Download downloads a file from a URL
|
||||
func (r *Request) Download(ctx context.Context, urlStr string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("failed to create download request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
for k, v := range r.config.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, utils.NewYandexMusicError("download failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, r.handleErrorResponse(resp.StatusCode, body)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// Search performs a multi-type search
|
||||
func (c *Client) Search(ctx context.Context, query string, searchType string, page, pageSize int) (*models.SearchResult, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": searchType,
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page_size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
|
||||
var result models.SearchResult
|
||||
if err := c.request.Get(ctx, "/search", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set client on all results
|
||||
if result.Tracks != nil {
|
||||
for i := range result.Tracks.Results {
|
||||
result.Tracks.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
if result.Albums != nil {
|
||||
for i := range result.Albums.Results {
|
||||
result.Albums.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
if result.Artists != nil {
|
||||
for i := range result.Artists.Results {
|
||||
result.Artists.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
if result.Playlists != nil {
|
||||
for i := range result.Playlists.Results {
|
||||
result.Playlists.Results[i].SetClient(c)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SearchTracks searches for tracks
|
||||
func (c *Client) SearchTracks(ctx context.Context, query string, page, pageSize int) (*models.SearchTracksResult, error) {
|
||||
result, err := c.Search(ctx, query, "track", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Tracks, nil
|
||||
}
|
||||
|
||||
// SearchAlbums searches for albums
|
||||
func (c *Client) SearchAlbums(ctx context.Context, query string, page, pageSize int) (*models.SearchAlbumsResult, error) {
|
||||
result, err := c.Search(ctx, query, "album", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Albums, nil
|
||||
}
|
||||
|
||||
// SearchArtists searches for artists
|
||||
func (c *Client) SearchArtists(ctx context.Context, query string, page, pageSize int) (*models.SearchArtistsResult, error) {
|
||||
result, err := c.Search(ctx, query, "artist", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Artists, nil
|
||||
}
|
||||
|
||||
// SearchPlaylists searches for playlists
|
||||
func (c *Client) SearchPlaylists(ctx context.Context, query string, page, pageSize int) (*models.SearchPlaylistsResult, error) {
|
||||
result, err := c.Search(ctx, query, "playlist", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Playlists, nil
|
||||
}
|
||||
|
||||
// SearchVideos searches for videos
|
||||
func (c *Client) SearchVideos(ctx context.Context, query string, page, pageSize int) (*models.SearchVideosResult, error) {
|
||||
result, err := c.Search(ctx, query, "video", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Videos, nil
|
||||
}
|
||||
|
||||
// SearchUsers searches for users
|
||||
func (c *Client) SearchUsers(ctx context.Context, query string, page, pageSize int) (*models.SearchUsersResult, error) {
|
||||
result, err := c.Search(ctx, query, "user", page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Users, nil
|
||||
}
|
||||
|
||||
// SearchSuggestions gets search suggestions
|
||||
func (c *Client) SearchSuggestions(ctx context.Context, query string) ([]string, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": "suggest",
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Suggestions []string `json:"suggestions"`
|
||||
}
|
||||
if err := c.request.Get(ctx, "/search/suggest", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Suggestions, nil
|
||||
}
|
||||
|
||||
// SearchBanners gets search banners
|
||||
func (c *Client) SearchBanners(ctx context.Context, query string) ([]models.SearchBanner, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": "banner",
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Banners []models.SearchBanner `json:"banners"`
|
||||
}
|
||||
if err := c.request.Get(ctx, "/search/banner", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Banners, nil
|
||||
}
|
||||
|
||||
// SearchTags gets search tags
|
||||
func (c *Client) SearchTags(ctx context.Context, query string) ([]models.Tag, error) {
|
||||
params := map[string]string{
|
||||
"text": query,
|
||||
"type": "tag",
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tags []models.Tag `json:"tags"`
|
||||
}
|
||||
if err := c.request.Get(ctx, "/search/tag", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Tags, nil
|
||||
}
|
||||
|
||||
// SearchResult represents search results
|
||||
// This is defined in models/search.go
|
||||
@@ -0,0 +1,73 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
)
|
||||
|
||||
// GetSettings gets user settings
|
||||
func (c *Client) GetSettings(ctx context.Context) (*models.AccountSettings, error) {
|
||||
var result models.AccountSettings
|
||||
if err := c.request.Get(ctx, "/account/settings", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateSettings updates user settings
|
||||
func (c *Client) UpdateSettings(ctx context.Context, settings *models.AccountSettings) (*models.AccountSettings, error) {
|
||||
var result models.AccountSettings
|
||||
if err := c.request.Put(ctx, "/account/settings", settings, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetExperiments gets experiments
|
||||
func (c *Client) GetExperiments(ctx context.Context) (*models.Experiments, error) {
|
||||
var result models.Experiments
|
||||
if err := c.request.Get(ctx, "/experiments", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetExperimentDetails gets experiment details
|
||||
func (c *Client) GetExperimentDetails(ctx context.Context, experimentID string) (*models.ExperimentDetail, error) {
|
||||
params := map[string]string{
|
||||
"experiment_id": experimentID,
|
||||
}
|
||||
var result models.ExperimentDetail
|
||||
if err := c.request.Get(ctx, "/experiments/details", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPermissions gets user permissions
|
||||
func (c *Client) GetPermissions(ctx context.Context) (*models.Permissions, error) {
|
||||
var result models.Permissions
|
||||
if err := c.request.Get(ctx, "/permissions", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetPermissionAlerts gets permission alerts
|
||||
func (c *Client) GetPermissionAlerts(ctx context.Context) (*models.PermissionAlerts, error) {
|
||||
var result models.PermissionAlerts
|
||||
if err := c.request.Get(ctx, "/permission-alerts", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetContentRestrictions gets content restrictions
|
||||
func (c *Client) GetContentRestrictions(ctx context.Context) (*models.ContentRestrictions, error) {
|
||||
var result models.ContentRestrictions
|
||||
if err := c.request.Get(ctx, "/content-restrictions", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetTrackSupplement gets supplement for a track
|
||||
// GET /tracks/{track_id}/supplement
|
||||
func (c *Client) GetTrackSupplement(ctx context.Context, trackID string) (*models.Supplement, error) {
|
||||
path := fmt.Sprintf("/tracks/%s/supplement", trackID)
|
||||
|
||||
var result models.Supplement
|
||||
if err := c.request.Get(ctx, path, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetTrackLyrics gets lyrics for a track (new endpoint with signing)
|
||||
// GET /tracks/{track_id}/lyrics
|
||||
func (c *Client) GetTrackLyrics(ctx context.Context, trackID string, format string) (*models.TrackLyrics, error) {
|
||||
path := fmt.Sprintf("/tracks/%s/lyrics", trackID)
|
||||
|
||||
// Generate signature for the request
|
||||
sign := utils.GetSignRequest(trackID, utils.DefaultSignKey)
|
||||
|
||||
params := map[string]string{
|
||||
"format": format,
|
||||
"timeStamp": strconv.FormatInt(sign.Timestamp, 10),
|
||||
"sign": sign.Value,
|
||||
}
|
||||
|
||||
var result models.TrackLyrics
|
||||
if err := c.request.Get(ctx, path, params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetTrackLyricsText fetches the full lyrics text from the download URL
|
||||
func (c *Client) GetTrackLyricsText(ctx context.Context, trackLyrics *models.TrackLyrics) (string, error) {
|
||||
data, err := c.request.Download(ctx, trackLyrics.DownloadURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// GetLyrics gets lyrics info for a track (legacy endpoint)
|
||||
// GET /tracks/lyrics?track_id={track_id}
|
||||
func (c *Client) GetLyrics(ctx context.Context, trackID string) (*models.LyricsInfo, error) {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result models.LyricsInfo
|
||||
if err := c.request.Get(ctx, "/tracks/lyrics", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetVideoSupplement gets video supplement for a track (legacy endpoint)
|
||||
// GET /tracks/video-supplement?track_id={track_id}
|
||||
func (c *Client) GetVideoSupplement(ctx context.Context, trackID string) (*models.VideoSupplement, error) {
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result models.VideoSupplement
|
||||
if err := c.request.Get(ctx, "/tracks/video-supplement", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetLyricsByTrackID is an alias for GetTrackLyrics for backward compatibility
|
||||
// GET /tracks/{track_id}/lyrics
|
||||
func (c *Client) GetLyricsByTrackID(ctx context.Context, trackID string, format string) (*models.TrackLyrics, error) {
|
||||
return c.GetTrackLyrics(ctx, trackID, format)
|
||||
}
|
||||
|
||||
// LyricsFormat represents the lyrics format
|
||||
type LyricsFormat string
|
||||
|
||||
const (
|
||||
// LyricsFormatText is plain text format
|
||||
LyricsFormatText LyricsFormat = "TEXT"
|
||||
// LyricsFormatLRC is LRC format with timestamps
|
||||
LyricsFormatLRC LyricsFormat = "LRC"
|
||||
)
|
||||
|
||||
// GetTrackLyricsWithOptions gets lyrics with options
|
||||
func (c *Client) GetTrackLyricsWithOptions(ctx context.Context, trackID string, format LyricsFormat) (*models.TrackLyrics, error) {
|
||||
return c.GetTrackLyrics(ctx, trackID, string(format))
|
||||
}
|
||||
|
||||
// SupplementOptions holds options for supplement requests
|
||||
type SupplementOptions struct {
|
||||
TrackID string
|
||||
}
|
||||
|
||||
// GetSupplement gets track supplement with options
|
||||
func (c *Client) GetSupplement(ctx context.Context, opts SupplementOptions) (*models.Supplement, error) {
|
||||
return c.GetTrackSupplement(ctx, opts.TrackID)
|
||||
}
|
||||
|
||||
// LyricsOptions holds options for lyrics requests
|
||||
type LyricsOptions struct {
|
||||
TrackID string
|
||||
Format LyricsFormat
|
||||
}
|
||||
|
||||
// GetLyricsWithOptions gets track lyrics with options
|
||||
func (c *Client) GetLyricsWithOptions(ctx context.Context, opts LyricsOptions) (*models.TrackLyrics, error) {
|
||||
format := string(opts.Format)
|
||||
if format == "" {
|
||||
format = string(LyricsFormatText)
|
||||
}
|
||||
return c.GetTrackLyrics(ctx, opts.TrackID, format)
|
||||
}
|
||||
|
||||
// Sign represents a request signature
|
||||
type Sign struct {
|
||||
Timestamp int64
|
||||
Value string
|
||||
}
|
||||
|
||||
// GetSignRequest creates a signature for a track request (exported for external use)
|
||||
func GetSignRequest(trackID string) Sign {
|
||||
h := utils.GetSignRequest(trackID, utils.DefaultSignKey)
|
||||
|
||||
return Sign{
|
||||
Timestamp: h.Timestamp,
|
||||
Value: h.Value,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
// testServer creates a test HTTP server with the given handler
|
||||
func testServer(handler http.HandlerFunc) *httptest.Server {
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
// newTestClient creates a client pointing to the test server
|
||||
func newTestClient(serverURL string) *Client {
|
||||
return NewClient(ClientConfig{
|
||||
BaseURL: serverURL,
|
||||
Token: "test-token",
|
||||
Language: "en",
|
||||
Timeout: 5,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// GetTracks gets tracks by IDs
|
||||
func (c *Client) GetTracks(ctx context.Context, trackIDs []string) ([]*models.Track, error) {
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"track-ids": joinIDs(trackIDs),
|
||||
}
|
||||
|
||||
var result []models.Track
|
||||
if err := c.request.Get(ctx, "/tracks", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks := make([]*models.Track, len(result))
|
||||
for i := range result {
|
||||
result[i].SetClient(c)
|
||||
tracks[i] = &result[i]
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// GetTrack gets a single track by ID
|
||||
func (c *Client) GetTrack(ctx context.Context, trackID string) (*models.Track, error) {
|
||||
tracks, err := c.GetTracks(ctx, []string{trackID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tracks) == 0 {
|
||||
return nil, utils.NewYandexMusicError("track not found", utils.ErrNotFound)
|
||||
}
|
||||
return tracks[0], nil
|
||||
}
|
||||
|
||||
// GetTrackDownloadInfo gets download info for a track
|
||||
func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality string) ([]models.DownloadInfo, error) {
|
||||
// This requires signing - use the sign utility
|
||||
// For now, return basic implementation
|
||||
params := map[string]string{
|
||||
"track_id": trackID,
|
||||
}
|
||||
|
||||
var result []models.DownloadInfo
|
||||
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DownloadTrack downloads a track
|
||||
func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitrate int) ([]byte, error) {
|
||||
info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find matching download info
|
||||
var url string
|
||||
for _, di := range info {
|
||||
if di.Codec == format && (bitrate == 0 || di.Bitrate == bitrate) {
|
||||
url = di.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if url == "" && len(info) > 0 {
|
||||
url = info[0].URL
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
return nil, utils.NewYandexMusicError("no download URL available", utils.ErrNotFound)
|
||||
}
|
||||
|
||||
return c.request.Download(ctx, url)
|
||||
}
|
||||
|
||||
// SearchResult represents search results
|
||||
type SearchResult struct {
|
||||
Tracks *SearchTracksResult `json:"tracks"`
|
||||
}
|
||||
|
||||
// SearchTracksResult represents track search results
|
||||
type SearchTracksResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []models.Track `json:"results"`
|
||||
}
|
||||
|
||||
func joinIDs(ids []string) string {
|
||||
result := ""
|
||||
for i, id := range ids {
|
||||
if i > 0 {
|
||||
result += ","
|
||||
}
|
||||
result += id
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
func TestClient_GetTracks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackIDs []string
|
||||
mockResponse []byte
|
||||
mockError error
|
||||
expectError bool
|
||||
expectedTracks int
|
||||
checkTrack func(*testing.T, *models.Track)
|
||||
}{
|
||||
{
|
||||
name: "single track",
|
||||
trackIDs: []string{"12345"},
|
||||
mockResponse: []byte(`{
|
||||
"result": [
|
||||
{
|
||||
"id": "12345",
|
||||
"title": "Test Track",
|
||||
"duration_ms": 180000,
|
||||
"storage_id": "67890",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"genre": "rock",
|
||||
"version": "album",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": -12.5,
|
||||
"track_sharing_flag": "allowed",
|
||||
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
|
||||
"albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
|
||||
"major": {"id": "major1", "name": "Major Label", "description": "Big label"},
|
||||
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true}
|
||||
}
|
||||
]
|
||||
}`),
|
||||
expectedTracks: 1,
|
||||
checkTrack: func(t *testing.T, tr *models.Track) {
|
||||
if tr.ID != "12345" {
|
||||
t.Errorf("Expected ID '12345', got '%s'", tr.ID)
|
||||
}
|
||||
if tr.Title != "Test Track" {
|
||||
t.Errorf("Expected Title 'Test Track', got '%s'", tr.Title)
|
||||
}
|
||||
if tr.DurationMs != 180000 {
|
||||
t.Errorf("Expected DurationMs 180000, got %d", tr.DurationMs)
|
||||
}
|
||||
if len(tr.Artists) != 1 {
|
||||
t.Errorf("Expected 1 artist, got %d", len(tr.Artists))
|
||||
}
|
||||
if tr.Major == nil || tr.Major.Name != "Major Label" {
|
||||
t.Errorf("Expected Major 'Major Label'")
|
||||
}
|
||||
if tr.Lyrics == nil || tr.Lyrics.Text != "Test lyrics" {
|
||||
t.Errorf("Expected Lyrics 'Test lyrics'")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple tracks",
|
||||
trackIDs: []string{"12345", "67890"},
|
||||
mockResponse: []byte(`{
|
||||
"result": [
|
||||
{"id": "12345", "title": "Track 1", "duration_ms": 100000, "storage_id": "1", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []},
|
||||
{"id": "67890", "title": "Track 2", "duration_ms": 200000, "storage_id": "2", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []}
|
||||
]
|
||||
}`),
|
||||
expectedTracks: 2,
|
||||
checkTrack: func(t *testing.T, tr *models.Track) {
|
||||
if tr.ID != "12345" && tr.ID != "67890" {
|
||||
t.Errorf("Unexpected track ID: %s", tr.ID)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty track IDs",
|
||||
trackIDs: []string{},
|
||||
mockResponse: nil,
|
||||
expectedTracks: 0,
|
||||
},
|
||||
{
|
||||
name: "nil track IDs",
|
||||
trackIDs: nil,
|
||||
mockResponse: nil,
|
||||
expectedTracks: 0,
|
||||
},
|
||||
{
|
||||
name: "API error",
|
||||
trackIDs: []string{"12345"},
|
||||
mockError: utils.NewYandexMusicError("not found", utils.ErrNotFound),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty result",
|
||||
trackIDs: []string{"nonexistent"},
|
||||
mockResponse: []byte(`{"result": []}`),
|
||||
expectedTracks: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var server *httptest.Server
|
||||
if tt.mockError != nil || tt.mockResponse != nil {
|
||||
server = testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tracks" {
|
||||
t.Errorf("Expected path '/tracks', got '%s'", r.URL.Path)
|
||||
}
|
||||
if tt.mockError != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error": "not found"}`))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(tt.mockResponse)
|
||||
})
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
var client *Client
|
||||
if server != nil {
|
||||
client = newTestClient(server.URL)
|
||||
} else {
|
||||
client = newTestClient("http://localhost:9999")
|
||||
}
|
||||
|
||||
tracks, err := client.GetTracks(context.Background(), tt.trackIDs)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(tracks) != tt.expectedTracks {
|
||||
t.Errorf("Expected %d tracks, got %d", tt.expectedTracks, len(tracks))
|
||||
return
|
||||
}
|
||||
|
||||
if tt.checkTrack != nil {
|
||||
for _, track := range tracks {
|
||||
tt.checkTrack(t, track)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify client is set on tracks
|
||||
for _, track := range tracks {
|
||||
if track.GetClient() == nil {
|
||||
t.Error("Client not set on track")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetTrack(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trackID string
|
||||
mockResponse []byte
|
||||
mockError error
|
||||
expectError bool
|
||||
checkTrack func(*testing.T, *models.Track)
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
trackID: "12345",
|
||||
mockResponse: []byte(`{
|
||||
"result": [
|
||||
{
|
||||
"id": "12345",
|
||||
"title": "Test Track",
|
||||
"duration_ms": 180000,
|
||||
"storage_id": "67890",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"genre": "rock",
|
||||
"version": "album",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": -12.5,
|
||||
"track_sharing_flag": "allowed",
|
||||
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
|
||||
"albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
|
||||
"major": {"id": "major1", "name": "Major Label", "description": "Big label"},
|
||||
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true}
|
||||
}
|
||||
]
|
||||
}`),
|
||||
checkTrack: func(t *testing.T, tr *models.Track) {
|
||||
if tr.ID != "12345" {
|
||||
t.Errorf("Expected ID '12345', got '%s'", tr.ID)
|
||||
}
|
||||
if tr.Title != "Test Track" {
|
||||
t.Errorf("Expected Title 'Test Track', got '%s'", tr.Title)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
trackID: "nonexistent",
|
||||
mockResponse: []byte(`{"result": []}`),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "API error",
|
||||
trackID: "12345",
|
||||
mockError: utils.NewYandexMusicError("unauthorized", utils.ErrUnauthorized),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var server *httptest.Server
|
||||
if tt.mockError != nil || tt.mockResponse != nil {
|
||||
server = testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tracks" {
|
||||
t.Errorf("Expected path '/tracks', got '%s'", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("track-ids") != tt.trackID {
|
||||
t.Errorf("Expected track-ids '%s', got '%s'", tt.trackID, r.URL.Query().Get("track-ids"))
|
||||
}
|
||||
if tt.mockError != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error": "unauthorized"}`))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(tt.mockResponse)
|
||||
})
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
var client *Client
|
||||
if server != nil {
|
||||
client = newTestClient(server.URL)
|
||||
} else {
|
||||
client = newTestClient("http://localhost:9999")
|
||||
}
|
||||
|
||||
track, err := client.GetTrack(context.Background(), tt.trackID)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if track == nil {
|
||||
t.Fatal("Expected track, got nil")
|
||||
}
|
||||
|
||||
if tt.checkTrack != nil {
|
||||
tt.checkTrack(t, track)
|
||||
}
|
||||
|
||||
if track.GetClient() == nil {
|
||||
t.Error("Client not set on track")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SearchTracks(t *testing.T) {
|
||||
// Create test server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/search" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
if query.Get("type") != "track" {
|
||||
t.Errorf("Expected type=track, got %s", query.Get("type"))
|
||||
}
|
||||
|
||||
page := query.Get("page")
|
||||
pageSize := query.Get("page_size")
|
||||
|
||||
response := map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"tracks": map[string]interface{}{
|
||||
"total": 2,
|
||||
"results": []map[string]interface{}{
|
||||
{
|
||||
"id": "1",
|
||||
"title": "Track 1",
|
||||
"duration_ms": 100000,
|
||||
"storage_id": "1",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"title": "Track 2",
|
||||
"duration_ms": 200000,
|
||||
"storage_id": "2",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Verify pagination
|
||||
if page != "1" || pageSize != "20" {
|
||||
t.Errorf("Unexpected pagination: page=%s, page_size=%s", page, pageSize)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create client with test server URL
|
||||
config := ClientConfig{
|
||||
BaseURL: server.URL,
|
||||
Token: "test-token",
|
||||
}
|
||||
client := NewClient(config)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := client.SearchTracks(ctx, "test query", 1, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchTracks failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected search result, got nil")
|
||||
}
|
||||
|
||||
if result.Total != 2 {
|
||||
t.Errorf("Expected total 2, got %d", result.Total)
|
||||
}
|
||||
|
||||
if len(result.Results) != 2 {
|
||||
t.Errorf("Expected 2 results, got %d", len(result.Results))
|
||||
}
|
||||
|
||||
for i, track := range result.Results {
|
||||
if track.GetClient() == nil {
|
||||
t.Errorf("Track %d: Client not set", i)
|
||||
}
|
||||
if track.ID != "1" && track.ID != "2" {
|
||||
t.Errorf("Track %d: Unexpected ID %s", i, track.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SearchTracks_Mocked(t *testing.T) {
|
||||
server := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/search" {
|
||||
t.Errorf("Unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
return
|
||||
}
|
||||
if r.URL.Query().Get("type") != "track" {
|
||||
t.Errorf("Expected type=track, got %s", r.URL.Query().Get("type"))
|
||||
}
|
||||
|
||||
mockSearchResult := map[string]interface{}{
|
||||
"result": map[string]interface{}{
|
||||
"tracks": map[string]interface{}{
|
||||
"total": 2,
|
||||
"results": []map[string]interface{}{
|
||||
{
|
||||
"id": "1",
|
||||
"title": "Track 1",
|
||||
"duration_ms": 100000,
|
||||
"storage_id": "1",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"title": "Track 2",
|
||||
"duration_ms": 200000,
|
||||
"storage_id": "2",
|
||||
"cover_uri": "",
|
||||
"genre": "",
|
||||
"version": "",
|
||||
"explicit": false,
|
||||
"available": true,
|
||||
"available_for_premium_users": true,
|
||||
"r128": 0,
|
||||
"track_sharing_flag": "",
|
||||
"artists": []interface{}{},
|
||||
"albums": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mockSearchResult)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newTestClient(server.URL)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := client.SearchTracks(ctx, "test query", 1, 20)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SearchTracks failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected search result, got nil")
|
||||
}
|
||||
|
||||
if result.Total != 2 {
|
||||
t.Errorf("Expected total 2, got %d", result.Total)
|
||||
}
|
||||
|
||||
if len(result.Results) != 2 {
|
||||
t.Errorf("Expected 2 results, got %d", len(result.Results))
|
||||
}
|
||||
|
||||
for i, track := range result.Results {
|
||||
if track.GetClient() == nil {
|
||||
t.Errorf("Track %d: Client not set", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Test error case
|
||||
errorServer := testServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error": "network error"}`))
|
||||
})
|
||||
defer errorServer.Close()
|
||||
|
||||
errorClient := newTestClient(errorServer.URL)
|
||||
_, err = errorClient.SearchTracks(ctx, "test", 1, 20)
|
||||
if err == nil {
|
||||
t.Error("Expected error for network failure")
|
||||
}
|
||||
if !utils.IsNetworkError(err) {
|
||||
t.Errorf("Expected network error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected string
|
||||
}{
|
||||
{"empty", []string{}, ""},
|
||||
{"single", []string{"123"}, "123"},
|
||||
{"multiple", []string{"123", "456", "789"}, "123,456,789"},
|
||||
{"with empty", []string{"123", "", "456"}, "123,,456"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := joinIDs(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("joinIDs(%v) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// AccountSettings represents account settings
|
||||
type AccountSettings struct {
|
||||
BaseModel
|
||||
Language string `json:"language"`
|
||||
Region string `json:"region"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Notifications bool `json:"notifications"`
|
||||
AutoPlay bool `json:"auto_play"`
|
||||
HighQuality bool `json:"high_quality"`
|
||||
CacheSize int `json:"cache_size"`
|
||||
DownloadQuality string `json:"download_quality"`
|
||||
ExplicitContent bool `json:"explicit_content"`
|
||||
NormalizeVolume bool `json:"normalize_volume"`
|
||||
Crossfade bool `json:"crossfade"`
|
||||
GaplessPlayback bool `json:"gapless_playback"`
|
||||
ShowLyrics bool `json:"show_lyrics"`
|
||||
ShowCredits bool `json:"show_credits"`
|
||||
AutoDownload bool `json:"auto_download"`
|
||||
DownloadOnWifiOnly bool `json:"download_on_wifi_only"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes AccountSettings from JSON
|
||||
func (a *AccountSettings) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings := &AccountSettings{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["language"]; ok {
|
||||
json.Unmarshal(v, &settings.Language)
|
||||
}
|
||||
if v, ok := raw["region"]; ok {
|
||||
json.Unmarshal(v, &settings.Region)
|
||||
}
|
||||
if v, ok := raw["email"]; ok {
|
||||
json.Unmarshal(v, &settings.Email)
|
||||
}
|
||||
if v, ok := raw["phone"]; ok {
|
||||
json.Unmarshal(v, &settings.Phone)
|
||||
}
|
||||
if v, ok := raw["notifications"]; ok {
|
||||
json.Unmarshal(v, &settings.Notifications)
|
||||
}
|
||||
if v, ok := raw["auto_play"]; ok {
|
||||
json.Unmarshal(v, &settings.AutoPlay)
|
||||
}
|
||||
if v, ok := raw["high_quality"]; ok {
|
||||
json.Unmarshal(v, &settings.HighQuality)
|
||||
}
|
||||
if v, ok := raw["cache_size"]; ok {
|
||||
json.Unmarshal(v, &settings.CacheSize)
|
||||
}
|
||||
if v, ok := raw["download_quality"]; ok {
|
||||
json.Unmarshal(v, &settings.DownloadQuality)
|
||||
}
|
||||
if v, ok := raw["explicit_content"]; ok {
|
||||
json.Unmarshal(v, &settings.ExplicitContent)
|
||||
}
|
||||
if v, ok := raw["normalize_volume"]; ok {
|
||||
json.Unmarshal(v, &settings.NormalizeVolume)
|
||||
}
|
||||
if v, ok := raw["crossfade"]; ok {
|
||||
json.Unmarshal(v, &settings.Crossfade)
|
||||
}
|
||||
if v, ok := raw["gapless_playback"]; ok {
|
||||
json.Unmarshal(v, &settings.GaplessPlayback)
|
||||
}
|
||||
if v, ok := raw["show_lyrics"]; ok {
|
||||
json.Unmarshal(v, &settings.ShowLyrics)
|
||||
}
|
||||
if v, ok := raw["show_credits"]; ok {
|
||||
json.Unmarshal(v, &settings.ShowCredits)
|
||||
}
|
||||
if v, ok := raw["auto_download"]; ok {
|
||||
json.Unmarshal(v, &settings.AutoDownload)
|
||||
}
|
||||
if v, ok := raw["download_on_wifi_only"]; ok {
|
||||
json.Unmarshal(v, &settings.DownloadOnWifiOnly)
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of AccountSettings
|
||||
func (a *AccountSettings) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
settings := &AccountSettings{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, settings); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, settings)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Subscription represents subscription info
|
||||
type Subscription struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
Price int `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Features []string `json:"features"`
|
||||
CanCancel bool `json:"can_cancel"`
|
||||
CanUpgrade bool `json:"can_upgrade"`
|
||||
TrialAvailable bool `json:"trial_available"`
|
||||
TrialUsed bool `json:"trial_used"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes Subscription from JSON
|
||||
func (s *Subscription) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sub := &Subscription{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &sub.ID)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &sub.Type)
|
||||
}
|
||||
if v, ok := raw["status"]; ok {
|
||||
json.Unmarshal(v, &sub.Status)
|
||||
}
|
||||
if v, ok := raw["start_date"]; ok {
|
||||
json.Unmarshal(v, &sub.StartDate)
|
||||
}
|
||||
if v, ok := raw["end_date"]; ok {
|
||||
json.Unmarshal(v, &sub.EndDate)
|
||||
}
|
||||
if v, ok := raw["auto_renew"]; ok {
|
||||
json.Unmarshal(v, &sub.AutoRenew)
|
||||
}
|
||||
if v, ok := raw["payment_method"]; ok {
|
||||
json.Unmarshal(v, &sub.PaymentMethod)
|
||||
}
|
||||
if v, ok := raw["price"]; ok {
|
||||
json.Unmarshal(v, &sub.Price)
|
||||
}
|
||||
if v, ok := raw["currency"]; ok {
|
||||
json.Unmarshal(v, &sub.Currency)
|
||||
}
|
||||
if v, ok := raw["features"]; ok {
|
||||
json.Unmarshal(v, &sub.Features)
|
||||
}
|
||||
if v, ok := raw["can_cancel"]; ok {
|
||||
json.Unmarshal(v, &sub.CanCancel)
|
||||
}
|
||||
if v, ok := raw["can_upgrade"]; ok {
|
||||
json.Unmarshal(v, &sub.CanUpgrade)
|
||||
}
|
||||
if v, ok := raw["trial_available"]; ok {
|
||||
json.Unmarshal(v, &sub.TrialAvailable)
|
||||
}
|
||||
if v, ok := raw["trial_used"]; ok {
|
||||
json.Unmarshal(v, &sub.TrialUsed)
|
||||
}
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Subscriptions
|
||||
func (s *Subscription) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
sub := &Subscription{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, sub); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, sub)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PlusInfo represents Plus subscription info (from /account/plus or account status)
|
||||
type PlusInfo struct {
|
||||
BaseModel
|
||||
HasPlus bool `json:"has_plus"`
|
||||
IsTutorialCompleted bool `json:"is_tutorial_completed"`
|
||||
Migrated *bool `json:"migrated,omitempty"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes PlusInfo from JSON
|
||||
func (p *PlusInfo) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plus := &PlusInfo{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["has_plus"]; ok {
|
||||
json.Unmarshal(v, &plus.HasPlus)
|
||||
}
|
||||
if v, ok := raw["is_tutorial_completed"]; ok {
|
||||
json.Unmarshal(v, &plus.IsTutorialCompleted)
|
||||
}
|
||||
if v, ok := raw["migrated"]; ok {
|
||||
var val bool
|
||||
json.Unmarshal(v, &val)
|
||||
plus.Migrated = &val
|
||||
}
|
||||
|
||||
return plus, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of PlusInfo
|
||||
func (p *PlusInfo) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
plus := &PlusInfo{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, plus); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, plus)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PromoCodeStatus represents promo code status
|
||||
type PromoCodeStatus struct {
|
||||
BaseModel
|
||||
Code string `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
Discount int `json:"discount"`
|
||||
Months int `json:"months"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Activated bool `json:"activated"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes PromoCodeStatus from JSON
|
||||
func (p *PromoCodeStatus) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
promo := &PromoCodeStatus{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["code"]; ok {
|
||||
json.Unmarshal(v, &promo.Code)
|
||||
}
|
||||
if v, ok := raw["status"]; ok {
|
||||
json.Unmarshal(v, &promo.Status)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &promo.Description)
|
||||
}
|
||||
if v, ok := raw["discount"]; ok {
|
||||
json.Unmarshal(v, &promo.Discount)
|
||||
}
|
||||
if v, ok := raw["months"]; ok {
|
||||
json.Unmarshal(v, &promo.Months)
|
||||
}
|
||||
if v, ok := raw["expires_at"]; ok {
|
||||
json.Unmarshal(v, &promo.ExpiresAt)
|
||||
}
|
||||
if v, ok := raw["activated"]; ok {
|
||||
json.Unmarshal(v, &promo.Activated)
|
||||
}
|
||||
|
||||
return promo, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of PromoCodeStatus
|
||||
func (p *PromoCodeStatus) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
promo := &PromoCodeStatus{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, promo); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, promo)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Album represents a full album
|
||||
type Album struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Genre string `json:"genre"`
|
||||
Year int `json:"year"`
|
||||
TrackCount int `json:"track_count"`
|
||||
Available bool `json:"available"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Artists []Artist `json:"artists"`
|
||||
Labels []Label `json:"labels"`
|
||||
Volumes [][]Track `json:"volumes"`
|
||||
Description string `json:"description"`
|
||||
Meta *AlbumMeta `json:"meta"`
|
||||
Best *Best `json:"best"`
|
||||
// Additional fields
|
||||
Region string `json:"region"`
|
||||
AvailableForPremiumUsers bool `json:"available_for_premium_users"`
|
||||
AvailableForPremiumUsersOnly bool `json:"available_for_premium_users_only"`
|
||||
ContentWarning string `json:"content_warning"`
|
||||
Label string `json:"label"`
|
||||
CatalogNumber string `json:"catalog_number"`
|
||||
Options []string `json:"options"`
|
||||
TrackPositions []TrackPosition `json:"track_positions"`
|
||||
SimilarAlbums []Album `json:"similar_albums"`
|
||||
Trailer *TrailerInfo `json:"trailer"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes an Album from JSON
|
||||
func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
album := &Album{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
// Simple fields
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &album.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &album.Title)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &album.CoverURI)
|
||||
}
|
||||
if v, ok := raw["genre"]; ok {
|
||||
json.Unmarshal(v, &album.Genre)
|
||||
}
|
||||
if v, ok := raw["year"]; ok {
|
||||
json.Unmarshal(v, &album.Year)
|
||||
}
|
||||
if v, ok := raw["track_count"]; ok {
|
||||
json.Unmarshal(v, &album.TrackCount)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &album.Available)
|
||||
}
|
||||
if v, ok := raw["release_date"]; ok {
|
||||
json.Unmarshal(v, &album.ReleaseDate)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &album.Description)
|
||||
}
|
||||
if v, ok := raw["region"]; ok {
|
||||
json.Unmarshal(v, &album.Region)
|
||||
}
|
||||
if v, ok := raw["available_for_premium_users"]; ok {
|
||||
json.Unmarshal(v, &album.AvailableForPremiumUsers)
|
||||
}
|
||||
if v, ok := raw["available_for_premium_users_only"]; ok {
|
||||
json.Unmarshal(v, &album.AvailableForPremiumUsersOnly)
|
||||
}
|
||||
if v, ok := raw["content_warning"]; ok {
|
||||
json.Unmarshal(v, &album.ContentWarning)
|
||||
}
|
||||
if v, ok := raw["label"]; ok {
|
||||
json.Unmarshal(v, &album.Label)
|
||||
}
|
||||
if v, ok := raw["catalog_number"]; ok {
|
||||
json.Unmarshal(v, &album.CatalogNumber)
|
||||
}
|
||||
if v, ok := raw["options"]; ok {
|
||||
json.Unmarshal(v, &album.Options)
|
||||
}
|
||||
|
||||
// Nested objects
|
||||
if v, ok := raw["artists"]; ok {
|
||||
var artists []Artist
|
||||
json.Unmarshal(v, &artists)
|
||||
album.Artists = artists
|
||||
}
|
||||
if v, ok := raw["labels"]; ok {
|
||||
var labels []Label
|
||||
json.Unmarshal(v, &labels)
|
||||
album.Labels = labels
|
||||
}
|
||||
if v, ok := raw["volumes"]; ok {
|
||||
var volumes [][]Track
|
||||
json.Unmarshal(v, &volumes)
|
||||
album.Volumes = volumes
|
||||
}
|
||||
if v, ok := raw["meta"]; ok {
|
||||
var meta AlbumMeta
|
||||
json.Unmarshal(v, &meta)
|
||||
album.Meta = &meta
|
||||
}
|
||||
if v, ok := raw["best"]; ok {
|
||||
var best Best
|
||||
json.Unmarshal(v, &best)
|
||||
album.Best = &best
|
||||
}
|
||||
if v, ok := raw["track_positions"]; ok {
|
||||
var positions []TrackPosition
|
||||
json.Unmarshal(v, &positions)
|
||||
album.TrackPositions = positions
|
||||
}
|
||||
if v, ok := raw["similar_albums"]; ok {
|
||||
var similar []Album
|
||||
json.Unmarshal(v, &similar)
|
||||
album.SimilarAlbums = similar
|
||||
}
|
||||
if v, ok := raw["trailer"]; ok {
|
||||
var trailer TrailerInfo
|
||||
json.Unmarshal(v, &trailer)
|
||||
album.Trailer = &trailer
|
||||
}
|
||||
|
||||
return album, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Albums
|
||||
func (a *Album) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
album := &Album{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, album); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, album)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AlbumMeta represents album metadata
|
||||
type AlbumMeta struct {
|
||||
BaseModel
|
||||
Availability *Availability `json:"availability"`
|
||||
Upsell *Upsell `json:"upsell"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes an AlbumMeta from JSON
|
||||
func (am *AlbumMeta) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta := &AlbumMeta{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["availability"]; ok {
|
||||
var avail Availability
|
||||
json.Unmarshal(v, &avail)
|
||||
meta.Availability = &avail
|
||||
}
|
||||
if v, ok := raw["upsell"]; ok {
|
||||
var upsell Upsell
|
||||
json.Unmarshal(v, &upsell)
|
||||
meta.Upsell = &upsell
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of AlbumMeta
|
||||
func (am *AlbumMeta) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
meta := &AlbumMeta{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, meta); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, meta)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Availability represents availability info
|
||||
type Availability struct {
|
||||
AvailableForPremiumUsers bool `json:"available_for_premium_users"`
|
||||
Territories []string `json:"territories"`
|
||||
}
|
||||
|
||||
// Upsell represents upsell info
|
||||
type Upsell struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Best represents best tracks info
|
||||
type Best struct {
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
// TrailerInfo represents album/track trailer info
|
||||
type TrailerInfo struct {
|
||||
BaseModel
|
||||
PreviewURI string `json:"preview_uri"`
|
||||
TrackID string `json:"track_id"`
|
||||
AlbumID string `json:"album_id"`
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a TrailerInfo from JSON
|
||||
func (ti *TrailerInfo) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trailer := &TrailerInfo{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["preview_uri"]; ok {
|
||||
json.Unmarshal(v, &trailer.PreviewURI)
|
||||
}
|
||||
if v, ok := raw["track_id"]; ok {
|
||||
json.Unmarshal(v, &trailer.TrackID)
|
||||
}
|
||||
if v, ok := raw["album_id"]; ok {
|
||||
json.Unmarshal(v, &trailer.AlbumID)
|
||||
}
|
||||
if v, ok := raw["duration"]; ok {
|
||||
json.Unmarshal(v, &trailer.Duration)
|
||||
}
|
||||
|
||||
return trailer, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of TrailerInfo
|
||||
func (ti *TrailerInfo) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
trailer := &TrailerInfo{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, trailer); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, trailer)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAlbum_DeJSON(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`{
|
||||
"id": "123",
|
||||
"title": "Test Album",
|
||||
"cover_uri": "https://example.com/cover.jpg",
|
||||
"genre": "rock",
|
||||
"year": 2023,
|
||||
"track_count": 10,
|
||||
"available": true,
|
||||
"release_date": "2023-01-15",
|
||||
"description": "Test album description",
|
||||
"region": "RU",
|
||||
"available_for_premium_users": true,
|
||||
"available_for_premium_users_only": false,
|
||||
"content_warning": "explicit",
|
||||
"label": "Test Label",
|
||||
"catalog_number": "CAT-123",
|
||||
"options": ["option1", "option2"],
|
||||
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
|
||||
"labels": [{"id": "label1", "name": "Label 1"}],
|
||||
"volumes": [[{"id": "t1", "title": "Track 1", "duration_ms": 180000}]],
|
||||
"meta": {"availability": {"available_for_premium_users": true, "territories": ["RU", "US"]}},
|
||||
"best": {"tracks": [{"id": "bt1", "title": "Best Track", "duration_ms": 200000}]},
|
||||
"track_positions": [{"revision": 1, "timestamp": "2023-01-01T00:00:00Z", "position": 1, "from_cache": false}],
|
||||
"similar_albums": [{"id": "sim1", "title": "Similar Album", "cover_uri": "https://example.com/sim.jpg", "year": 2022}],
|
||||
"trailer": {"preview_uri": "https://example.com/preview.mp3", "track_id": "t1", "album_id": "123", "duration": 30}
|
||||
}`)
|
||||
|
||||
album := &Album{BaseModel: *NewBaseModel(client)}
|
||||
result, err := album.DeJSON(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeJSON failed: %v", err)
|
||||
}
|
||||
|
||||
resultAlbum := result.(*Album)
|
||||
if resultAlbum.ID != "123" {
|
||||
t.Errorf("Expected ID '123', got '%s'", resultAlbum.ID)
|
||||
}
|
||||
if resultAlbum.Title != "Test Album" {
|
||||
t.Errorf("Expected Title 'Test Album', got '%s'", resultAlbum.Title)
|
||||
}
|
||||
if resultAlbum.CoverURI != "https://example.com/cover.jpg" {
|
||||
t.Errorf("Expected CoverURI 'https://example.com/cover.jpg', got '%s'", resultAlbum.CoverURI)
|
||||
}
|
||||
if resultAlbum.Genre != "rock" {
|
||||
t.Errorf("Expected Genre 'rock', got '%s'", resultAlbum.Genre)
|
||||
}
|
||||
if resultAlbum.Year != 2023 {
|
||||
t.Errorf("Expected Year 2023, got %d", resultAlbum.Year)
|
||||
}
|
||||
if resultAlbum.TrackCount != 10 {
|
||||
t.Errorf("Expected TrackCount 10, got %d", resultAlbum.TrackCount)
|
||||
}
|
||||
if !resultAlbum.Available {
|
||||
t.Errorf("Expected Available true, got false")
|
||||
}
|
||||
if resultAlbum.ReleaseDate != "2023-01-15" {
|
||||
t.Errorf("Expected ReleaseDate '2023-01-15', got '%s'", resultAlbum.ReleaseDate)
|
||||
}
|
||||
if resultAlbum.Description != "Test album description" {
|
||||
t.Errorf("Expected Description 'Test album description', got '%s'", resultAlbum.Description)
|
||||
}
|
||||
if resultAlbum.Region != "RU" {
|
||||
t.Errorf("Expected Region 'RU', got '%s'", resultAlbum.Region)
|
||||
}
|
||||
if !resultAlbum.AvailableForPremiumUsers {
|
||||
t.Errorf("Expected AvailableForPremiumUsers true, got false")
|
||||
}
|
||||
if resultAlbum.AvailableForPremiumUsersOnly {
|
||||
t.Errorf("Expected AvailableForPremiumUsersOnly false, got true")
|
||||
}
|
||||
if resultAlbum.ContentWarning != "explicit" {
|
||||
t.Errorf("Expected ContentWarning 'explicit', got '%s'", resultAlbum.ContentWarning)
|
||||
}
|
||||
if resultAlbum.Label != "Test Label" {
|
||||
t.Errorf("Expected Label 'Test Label', got '%s'", resultAlbum.Label)
|
||||
}
|
||||
if resultAlbum.CatalogNumber != "CAT-123" {
|
||||
t.Errorf("Expected CatalogNumber 'CAT-123', got '%s'", resultAlbum.CatalogNumber)
|
||||
}
|
||||
if len(resultAlbum.Options) != 2 {
|
||||
t.Errorf("Expected 2 options, got %d", len(resultAlbum.Options))
|
||||
}
|
||||
|
||||
// Test nested artists
|
||||
if len(resultAlbum.Artists) != 1 {
|
||||
t.Errorf("Expected 1 artist, got %d", len(resultAlbum.Artists))
|
||||
}
|
||||
if resultAlbum.Artists[0].Name != "Artist 1" {
|
||||
t.Errorf("Expected artist name 'Artist 1', got '%s'", resultAlbum.Artists[0].Name)
|
||||
}
|
||||
|
||||
// Test nested labels
|
||||
if len(resultAlbum.Labels) != 1 {
|
||||
t.Errorf("Expected 1 label, got %d", len(resultAlbum.Labels))
|
||||
}
|
||||
if resultAlbum.Labels[0].Name != "Label 1" {
|
||||
t.Errorf("Expected label name 'Label 1', got '%s'", resultAlbum.Labels[0].Name)
|
||||
}
|
||||
|
||||
// Test volumes
|
||||
if len(resultAlbum.Volumes) != 1 {
|
||||
t.Errorf("Expected 1 volume, got %d", len(resultAlbum.Volumes))
|
||||
}
|
||||
if len(resultAlbum.Volumes[0]) != 1 {
|
||||
t.Errorf("Expected 1 track in volume, got %d", len(resultAlbum.Volumes[0]))
|
||||
}
|
||||
if resultAlbum.Volumes[0][0].Title != "Track 1" {
|
||||
t.Errorf("Expected track title 'Track 1', got '%s'", resultAlbum.Volumes[0][0].Title)
|
||||
}
|
||||
|
||||
// Test meta
|
||||
if resultAlbum.Meta == nil {
|
||||
t.Fatal("Expected Meta to be non-nil")
|
||||
}
|
||||
if resultAlbum.Meta.Availability == nil {
|
||||
t.Fatal("Expected Availability to be non-nil")
|
||||
}
|
||||
if !resultAlbum.Meta.Availability.AvailableForPremiumUsers {
|
||||
t.Errorf("Expected AvailableForPremiumUsers true in meta")
|
||||
}
|
||||
if len(resultAlbum.Meta.Availability.Territories) != 2 {
|
||||
t.Errorf("Expected 2 territories, got %d", len(resultAlbum.Meta.Availability.Territories))
|
||||
}
|
||||
|
||||
// Test best
|
||||
if resultAlbum.Best == nil {
|
||||
t.Fatal("Expected Best to be non-nil")
|
||||
}
|
||||
if len(resultAlbum.Best.Tracks) != 1 {
|
||||
t.Errorf("Expected 1 best track, got %d", len(resultAlbum.Best.Tracks))
|
||||
}
|
||||
if resultAlbum.Best.Tracks[0].Title != "Best Track" {
|
||||
t.Errorf("Expected best track title 'Best Track', got '%s'", resultAlbum.Best.Tracks[0].Title)
|
||||
}
|
||||
|
||||
// Test track positions
|
||||
if len(resultAlbum.TrackPositions) != 1 {
|
||||
t.Errorf("Expected 1 track position, got %d", len(resultAlbum.TrackPositions))
|
||||
}
|
||||
if resultAlbum.TrackPositions[0].Position != 1 {
|
||||
t.Errorf("Expected position 1, got %d", resultAlbum.TrackPositions[0].Position)
|
||||
}
|
||||
if resultAlbum.TrackPositions[0].Revision != 1 {
|
||||
t.Errorf("Expected revision 1, got %d", resultAlbum.TrackPositions[0].Revision)
|
||||
}
|
||||
|
||||
// Test similar albums
|
||||
if len(resultAlbum.SimilarAlbums) != 1 {
|
||||
t.Errorf("Expected 1 similar album, got %d", len(resultAlbum.SimilarAlbums))
|
||||
}
|
||||
if resultAlbum.SimilarAlbums[0].Title != "Similar Album" {
|
||||
t.Errorf("Expected similar album title 'Similar Album', got '%s'", resultAlbum.SimilarAlbums[0].Title)
|
||||
}
|
||||
|
||||
// Test trailer
|
||||
if resultAlbum.Trailer == nil {
|
||||
t.Fatal("Expected Trailer to be non-nil")
|
||||
}
|
||||
if resultAlbum.Trailer.PreviewURI != "https://example.com/preview.mp3" {
|
||||
t.Errorf("Expected PreviewURI 'https://example.com/preview.mp3', got '%s'", resultAlbum.Trailer.PreviewURI)
|
||||
}
|
||||
if resultAlbum.Trailer.TrackID != "t1" {
|
||||
t.Errorf("Expected TrackID 't1', got '%s'", resultAlbum.Trailer.TrackID)
|
||||
}
|
||||
if resultAlbum.Trailer.AlbumID != "123" {
|
||||
t.Errorf("Expected AlbumID '123', got '%s'", resultAlbum.Trailer.AlbumID)
|
||||
}
|
||||
if resultAlbum.Trailer.Duration != 30 {
|
||||
t.Errorf("Expected Duration 30, got %d", resultAlbum.Trailer.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbum_DeJSON_Minimal(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`{
|
||||
"id": "minimal",
|
||||
"title": "Minimal Album"
|
||||
}`)
|
||||
|
||||
album := &Album{BaseModel: *NewBaseModel(client)}
|
||||
result, err := album.DeJSON(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeJSON failed: %v", err)
|
||||
}
|
||||
|
||||
resultAlbum := result.(*Album)
|
||||
if resultAlbum.ID != "minimal" {
|
||||
t.Errorf("Expected ID 'minimal', got '%s'", resultAlbum.ID)
|
||||
}
|
||||
if resultAlbum.Title != "Minimal Album" {
|
||||
t.Errorf("Expected Title 'Minimal Album', got '%s'", resultAlbum.Title)
|
||||
}
|
||||
// Other fields should be zero values
|
||||
if resultAlbum.Year != 0 {
|
||||
t.Errorf("Expected Year 0, got %d", resultAlbum.Year)
|
||||
}
|
||||
if resultAlbum.Available {
|
||||
t.Errorf("Expected Available false, got true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbum_DeList(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`[
|
||||
{"id": "1", "title": "Album 1", "year": 2022, "track_count": 5, "available": true},
|
||||
{"id": "2", "title": "Album 2", "year": 2023, "track_count": 8, "available": true},
|
||||
{"id": "3", "title": "Album 3", "year": 2021, "track_count": 12, "available": false}
|
||||
]`)
|
||||
|
||||
album := &Album{BaseModel: *NewBaseModel(client)}
|
||||
result, err := album.DeList(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeList failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 3 {
|
||||
t.Errorf("Expected 3 albums, got %d", len(result))
|
||||
}
|
||||
|
||||
albums := make([]*Album, len(result))
|
||||
for i, a := range result {
|
||||
albums[i] = a.(*Album)
|
||||
}
|
||||
|
||||
if albums[0].ID != "1" || albums[0].Title != "Album 1" || albums[0].Year != 2022 {
|
||||
t.Errorf("Album 1 mismatch: %+v", albums[0])
|
||||
}
|
||||
if albums[1].ID != "2" || albums[1].Title != "Album 2" || albums[1].Year != 2023 {
|
||||
t.Errorf("Album 2 mismatch: %+v", albums[1])
|
||||
}
|
||||
if albums[2].ID != "3" || albums[2].Title != "Album 3" || albums[2].Year != 2021 {
|
||||
t.Errorf("Album 3 mismatch: %+v", albums[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbum_DeList_Empty(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`[]`)
|
||||
|
||||
album := &Album{BaseModel: *NewBaseModel(client)}
|
||||
result, err := album.DeList(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeList failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected 0 albums, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbumMeta_DeJSON(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`{
|
||||
"availability": {"available_for_premium_users": true, "territories": ["RU", "US", "EU"]},
|
||||
"upsell": {"type": "premium", "description": "Upgrade to premium for full access"}
|
||||
}`)
|
||||
|
||||
meta := &AlbumMeta{BaseModel: *NewBaseModel(client)}
|
||||
result, err := meta.DeJSON(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeJSON failed: %v", err)
|
||||
}
|
||||
|
||||
resultMeta := result.(*AlbumMeta)
|
||||
if resultMeta.Availability == nil {
|
||||
t.Fatal("Expected Availability to be non-nil")
|
||||
}
|
||||
if !resultMeta.Availability.AvailableForPremiumUsers {
|
||||
t.Errorf("Expected AvailableForPremiumUsers true")
|
||||
}
|
||||
if len(resultMeta.Availability.Territories) != 3 {
|
||||
t.Errorf("Expected 3 territories, got %d", len(resultMeta.Availability.Territories))
|
||||
}
|
||||
if resultMeta.Upsell == nil {
|
||||
t.Fatal("Expected Upsell to be non-nil")
|
||||
}
|
||||
if resultMeta.Upsell.Type != "premium" {
|
||||
t.Errorf("Expected Upsell Type 'premium', got '%s'", resultMeta.Upsell.Type)
|
||||
}
|
||||
if resultMeta.Upsell.Description != "Upgrade to premium for full access" {
|
||||
t.Errorf("Expected Upsell Description 'Upgrade to premium for full access', got '%s'", resultMeta.Upsell.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbumMeta_DeList(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`[
|
||||
{"availability": {"available_for_premium_users": true}},
|
||||
{"availability": {"available_for_premium_users": false}}
|
||||
]`)
|
||||
|
||||
meta := &AlbumMeta{BaseModel: *NewBaseModel(client)}
|
||||
result, err := meta.DeList(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeList failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 meta objects, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrailerInfo_DeJSON(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`{
|
||||
"preview_uri": "https://example.com/preview.mp3",
|
||||
"track_id": "track123",
|
||||
"album_id": "album456",
|
||||
"duration": 45
|
||||
}`)
|
||||
|
||||
trailer := &TrailerInfo{BaseModel: *NewBaseModel(client)}
|
||||
result, err := trailer.DeJSON(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeJSON failed: %v", err)
|
||||
}
|
||||
|
||||
resultTrailer := result.(*TrailerInfo)
|
||||
if resultTrailer.PreviewURI != "https://example.com/preview.mp3" {
|
||||
t.Errorf("Expected PreviewURI 'https://example.com/preview.mp3', got '%s'", resultTrailer.PreviewURI)
|
||||
}
|
||||
if resultTrailer.TrackID != "track123" {
|
||||
t.Errorf("Expected TrackID 'track123', got '%s'", resultTrailer.TrackID)
|
||||
}
|
||||
if resultTrailer.AlbumID != "album456" {
|
||||
t.Errorf("Expected AlbumID 'album456', got '%s'", resultTrailer.AlbumID)
|
||||
}
|
||||
if resultTrailer.Duration != 45 {
|
||||
t.Errorf("Expected Duration 45, got %d", resultTrailer.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrailerInfo_DeList(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`[
|
||||
{"preview_uri": "https://example.com/preview1.mp3", "track_id": "t1", "album_id": "a1", "duration": 30},
|
||||
{"preview_uri": "https://example.com/preview2.mp3", "track_id": "t2", "album_id": "a2", "duration": 45}
|
||||
]`)
|
||||
|
||||
trailer := &TrailerInfo{BaseModel: *NewBaseModel(client)}
|
||||
result, err := trailer.DeList(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeList failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 trailers, got %d", len(result))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Artist represents a full artist
|
||||
type Artist struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Genres []string `json:"genres"`
|
||||
Tracks int `json:"tracks"`
|
||||
Albums int `json:"albums"`
|
||||
Link string `json:"link"`
|
||||
Available bool `json:"available"`
|
||||
Description string `json:"description"`
|
||||
Composer bool `json:"composer"`
|
||||
Various bool `json:"various"`
|
||||
Decomposed []Artist `json:"decomposed"`
|
||||
Ratings *Ratings `json:"ratings"`
|
||||
Counts *Counts `json:"counts"`
|
||||
SocialLinks []SocialLink `json:"social_links"`
|
||||
// Additional fields
|
||||
IDForFrom string `json:"id_for_from"`
|
||||
Cover *Cover `json:"cover"`
|
||||
BriefInfo *ArtistBriefInfo `json:"brief_info"`
|
||||
Donations *ArtistDonations `json:"donations"`
|
||||
Concerts []Concert `json:"concerts"`
|
||||
Clips []Clip `json:"clips"`
|
||||
Similar []Artist `json:"similar"`
|
||||
AlbumsData []Album `json:"albums_data"`
|
||||
TracksData []Track `json:"tracks_data"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes an Artist from JSON
|
||||
func (a *Artist) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artist := &Artist{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
// Simple fields
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &artist.ID)
|
||||
}
|
||||
if v, ok := raw["name"]; ok {
|
||||
json.Unmarshal(v, &artist.Name)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &artist.CoverURI)
|
||||
}
|
||||
if v, ok := raw["genres"]; ok {
|
||||
json.Unmarshal(v, &artist.Genres)
|
||||
}
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
json.Unmarshal(v, &artist.Tracks)
|
||||
}
|
||||
if v, ok := raw["albums"]; ok {
|
||||
json.Unmarshal(v, &artist.Albums)
|
||||
}
|
||||
if v, ok := raw["link"]; ok {
|
||||
json.Unmarshal(v, &artist.Link)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &artist.Available)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &artist.Description)
|
||||
}
|
||||
if v, ok := raw["composer"]; ok {
|
||||
json.Unmarshal(v, &artist.Composer)
|
||||
}
|
||||
if v, ok := raw["various"]; ok {
|
||||
json.Unmarshal(v, &artist.Various)
|
||||
}
|
||||
if v, ok := raw["id_for_from"]; ok {
|
||||
json.Unmarshal(v, &artist.IDForFrom)
|
||||
}
|
||||
|
||||
// Nested objects
|
||||
if v, ok := raw["decomposed"]; ok {
|
||||
var decomposed []Artist
|
||||
json.Unmarshal(v, &decomposed)
|
||||
artist.Decomposed = decomposed
|
||||
}
|
||||
if v, ok := raw["ratings"]; ok {
|
||||
var ratings Ratings
|
||||
json.Unmarshal(v, &ratings)
|
||||
artist.Ratings = &ratings
|
||||
}
|
||||
if v, ok := raw["counts"]; ok {
|
||||
var counts Counts
|
||||
json.Unmarshal(v, &counts)
|
||||
artist.Counts = &counts
|
||||
}
|
||||
if v, ok := raw["social_links"]; ok {
|
||||
var links []SocialLink
|
||||
json.Unmarshal(v, &links)
|
||||
artist.SocialLinks = links
|
||||
}
|
||||
if v, ok := raw["cover"]; ok {
|
||||
var cover Cover
|
||||
json.Unmarshal(v, &cover)
|
||||
artist.Cover = &cover
|
||||
}
|
||||
if v, ok := raw["brief_info"]; ok {
|
||||
var brief ArtistBriefInfo
|
||||
json.Unmarshal(v, &brief)
|
||||
artist.BriefInfo = &brief
|
||||
}
|
||||
if v, ok := raw["donations"]; ok {
|
||||
var donations ArtistDonations
|
||||
json.Unmarshal(v, &donations)
|
||||
artist.Donations = &donations
|
||||
}
|
||||
if v, ok := raw["concerts"]; ok {
|
||||
var concerts []Concert
|
||||
json.Unmarshal(v, &concerts)
|
||||
artist.Concerts = concerts
|
||||
}
|
||||
if v, ok := raw["clips"]; ok {
|
||||
var clips []Clip
|
||||
json.Unmarshal(v, &clips)
|
||||
artist.Clips = clips
|
||||
}
|
||||
if v, ok := raw["similar"]; ok {
|
||||
var similar []Artist
|
||||
json.Unmarshal(v, &similar)
|
||||
artist.Similar = similar
|
||||
}
|
||||
if v, ok := raw["albums"]; ok {
|
||||
var albums []Album
|
||||
json.Unmarshal(v, &albums)
|
||||
artist.AlbumsData = albums
|
||||
}
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []Track
|
||||
json.Unmarshal(v, &tracks)
|
||||
artist.TracksData = tracks
|
||||
}
|
||||
|
||||
return artist, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Artists
|
||||
func (a *Artist) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
artist := &Artist{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, artist); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, artist)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Ratings represents artist ratings
|
||||
type Ratings struct {
|
||||
Rating float64 `json:"rating"`
|
||||
Votes int `json:"votes"`
|
||||
}
|
||||
|
||||
// Counts represents artist counts
|
||||
type Counts struct {
|
||||
Tracks int `json:"tracks"`
|
||||
Albums int `json:"albums"`
|
||||
Folders int `json:"folders"`
|
||||
}
|
||||
|
||||
// SocialLink represents a social media link
|
||||
type SocialLink struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Cover represents cover image info
|
||||
type Cover struct {
|
||||
URI string `json:"uri"`
|
||||
Type string `json:"type"`
|
||||
Prefix string `json:"prefix"`
|
||||
Pattern string `json:"pattern"`
|
||||
Background string `json:"background"`
|
||||
}
|
||||
|
||||
// ArtistBriefInfo represents brief artist info
|
||||
type ArtistBriefInfo struct {
|
||||
Description string `json:"description"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// ArtistDonations represents artist donations info
|
||||
type ArtistDonations struct {
|
||||
Items []DonationItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Goal int `json:"goal"`
|
||||
Raised int `json:"raised"`
|
||||
}
|
||||
|
||||
// DonationItem represents a donation item
|
||||
type DonationItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Amount int `json:"amount"`
|
||||
Goal int `json:"goal"`
|
||||
Raised int `json:"raised"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// ConcertShort represents a concert (minimal for artist)
|
||||
type ConcertShort struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Date string `json:"date"`
|
||||
Venue string `json:"venue"`
|
||||
Location string `json:"location"`
|
||||
TicketURI string `json:"ticket_uri"`
|
||||
ImageURI string `json:"image_uri"`
|
||||
MinPrice int `json:"min_price"`
|
||||
MaxPrice int `json:"max_price"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a ConcertShort from JSON
|
||||
func (c *ConcertShort) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
concert := &ConcertShort{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &concert.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &concert.Title)
|
||||
}
|
||||
if v, ok := raw["date"]; ok {
|
||||
json.Unmarshal(v, &concert.Date)
|
||||
}
|
||||
if v, ok := raw["venue"]; ok {
|
||||
json.Unmarshal(v, &concert.Venue)
|
||||
}
|
||||
if v, ok := raw["location"]; ok {
|
||||
json.Unmarshal(v, &concert.Location)
|
||||
}
|
||||
if v, ok := raw["ticket_uri"]; ok {
|
||||
json.Unmarshal(v, &concert.TicketURI)
|
||||
}
|
||||
if v, ok := raw["image_uri"]; ok {
|
||||
json.Unmarshal(v, &concert.ImageURI)
|
||||
}
|
||||
if v, ok := raw["min_price"]; ok {
|
||||
json.Unmarshal(v, &concert.MinPrice)
|
||||
}
|
||||
if v, ok := raw["max_price"]; ok {
|
||||
json.Unmarshal(v, &concert.MaxPrice)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &concert.Available)
|
||||
}
|
||||
|
||||
return concert, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of ConcertShort
|
||||
func (c *ConcertShort) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
concert := &ConcertShort{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, concert); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, concert)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ClipShort represents a clip (minimal for artist)
|
||||
type ClipShort struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Duration int `json:"duration"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
PreviewURI string `json:"preview_uri"`
|
||||
Available bool `json:"available"`
|
||||
Views int `json:"views"`
|
||||
Likes int `json:"likes"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a ClipShort from JSON
|
||||
func (c *ClipShort) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clip := &ClipShort{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &clip.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &clip.Title)
|
||||
}
|
||||
if v, ok := raw["duration"]; ok {
|
||||
json.Unmarshal(v, &clip.Duration)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &clip.CoverURI)
|
||||
}
|
||||
if v, ok := raw["preview_uri"]; ok {
|
||||
json.Unmarshal(v, &clip.PreviewURI)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &clip.Available)
|
||||
}
|
||||
if v, ok := raw["views"]; ok {
|
||||
json.Unmarshal(v, &clip.Views)
|
||||
}
|
||||
if v, ok := raw["likes"]; ok {
|
||||
json.Unmarshal(v, &clip.Likes)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &clip.CreatedAt)
|
||||
}
|
||||
|
||||
return clip, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of ClipShort
|
||||
func (c *ClipShort) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
clip := &ClipShort{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, clip); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, clip)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
|
||||
)
|
||||
|
||||
// Model is the base interface for all models
|
||||
type Model interface {
|
||||
// DeJSON deserializes from JSON
|
||||
DeJSON(data []byte, c ClientInterface) (Model, error)
|
||||
// DeList deserializes a list of models
|
||||
DeList(data []byte, c ClientInterface) ([]Model, error)
|
||||
// ToJSON serializes to JSON
|
||||
ToJSON() ([]byte, error)
|
||||
// ToMap converts to map
|
||||
ToMap() (map[string]any, error)
|
||||
}
|
||||
|
||||
// BaseModel provides common functionality for models
|
||||
type BaseModel struct {
|
||||
Client ClientInterface `json:"-"`
|
||||
}
|
||||
|
||||
// NewBaseModel creates a new BaseModel
|
||||
func NewBaseModel(c ClientInterface) *BaseModel {
|
||||
return &BaseModel{Client: c}
|
||||
}
|
||||
|
||||
// DeJSON implements Model.DeJSON for basic models
|
||||
func (b *BaseModel) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
// This should be overridden by concrete types
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeList implements Model.DeList for basic models
|
||||
func (b *BaseModel) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for range rawList {
|
||||
// This should be overridden by concrete types
|
||||
result = append(result, nil)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ToJSON serializes the model to JSON
|
||||
func (b *BaseModel) ToJSON() ([]byte, error) {
|
||||
return utils.Marshal(b)
|
||||
}
|
||||
|
||||
// ToMap converts the model to a map
|
||||
func (b *BaseModel) ToMap() (map[string]any, error) {
|
||||
data, err := b.ToJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetClient sets the client on the model
|
||||
func (b *BaseModel) SetClient(c ClientInterface) {
|
||||
b.Client = c
|
||||
}
|
||||
|
||||
// GetClient returns the client
|
||||
func (b *BaseModel) GetClient() ClientInterface {
|
||||
return b.Client
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// ClientInterface defines the minimal client interface needed by models
|
||||
type ClientInterface interface {
|
||||
GetRequest() RequestInterface
|
||||
}
|
||||
|
||||
// RequestInterface defines the minimal request interface needed by models
|
||||
type RequestInterface interface {
|
||||
Get(ctx context.Context, path string, params map[string]string, result any) error
|
||||
Post(ctx context.Context, path string, body any, result any) error
|
||||
Put(ctx context.Context, path string, body any, result any) error
|
||||
Delete(ctx context.Context, path string, body any, result any) error
|
||||
Download(ctx context.Context, url string) ([]byte, error)
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Clip represents a clip (short video)
|
||||
type Clip struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Duration int `json:"duration"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
PreviewURI string `json:"preview_uri"`
|
||||
Available bool `json:"available"`
|
||||
Views int `json:"views"`
|
||||
Likes int `json:"likes"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Artist *Artist `json:"artist"`
|
||||
Track *Track `json:"track"`
|
||||
Playlist *Playlist `json:"playlist"`
|
||||
ClipType string `json:"clip_type"`
|
||||
IsVertical bool `json:"is_vertical"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Clip from JSON
|
||||
func (c *Clip) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clip := &Clip{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &clip.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &clip.Title)
|
||||
}
|
||||
if v, ok := raw["duration"]; ok {
|
||||
json.Unmarshal(v, &clip.Duration)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &clip.CoverURI)
|
||||
}
|
||||
if v, ok := raw["preview_uri"]; ok {
|
||||
json.Unmarshal(v, &clip.PreviewURI)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &clip.Available)
|
||||
}
|
||||
if v, ok := raw["views"]; ok {
|
||||
json.Unmarshal(v, &clip.Views)
|
||||
}
|
||||
if v, ok := raw["likes"]; ok {
|
||||
json.Unmarshal(v, &clip.Likes)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &clip.CreatedAt)
|
||||
}
|
||||
if v, ok := raw["clip_type"]; ok {
|
||||
json.Unmarshal(v, &clip.ClipType)
|
||||
}
|
||||
if v, ok := raw["is_vertical"]; ok {
|
||||
json.Unmarshal(v, &clip.IsVertical)
|
||||
}
|
||||
if v, ok := raw["aspect_ratio"]; ok {
|
||||
json.Unmarshal(v, &clip.AspectRatio)
|
||||
}
|
||||
|
||||
if v, ok := raw["artist"]; ok {
|
||||
var artist Artist
|
||||
json.Unmarshal(v, &artist)
|
||||
clip.Artist = &artist
|
||||
}
|
||||
if v, ok := raw["track"]; ok {
|
||||
var track Track
|
||||
json.Unmarshal(v, &track)
|
||||
clip.Track = &track
|
||||
}
|
||||
if v, ok := raw["playlist"]; ok {
|
||||
var playlist Playlist
|
||||
json.Unmarshal(v, &playlist)
|
||||
clip.Playlist = &playlist
|
||||
}
|
||||
|
||||
return clip, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Clips
|
||||
func (c *Clip) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
clip := &Clip{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, clip); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, clip)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ClipsWillLike represents clips the user might like
|
||||
type ClipsWillLike struct {
|
||||
BaseModel
|
||||
Clips []Clip `json:"clips"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ArtistClipData represents artist clip data
|
||||
type ArtistClipData struct {
|
||||
BaseModel
|
||||
Clips []Clip `json:"clips"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ArtistClipItem represents an artist clip item
|
||||
type ArtistClipItem struct {
|
||||
BaseModel
|
||||
Clip *Clip `json:"clip"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
// Shot represents a shot (short video)
|
||||
type Shot struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Duration int `json:"duration"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
PreviewURI string `json:"preview_uri"`
|
||||
Available bool `json:"available"`
|
||||
Views int `json:"views"`
|
||||
Likes int `json:"likes"`
|
||||
Comments int `json:"comments"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Author *User `json:"author"`
|
||||
Track *Track `json:"track"`
|
||||
ShotType string `json:"shot_type"`
|
||||
Tags []string `json:"tags"`
|
||||
IsVertical bool `json:"is_vertical"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Shot from JSON
|
||||
func (s *Shot) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shot := &Shot{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &shot.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &shot.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &shot.Description)
|
||||
}
|
||||
if v, ok := raw["duration"]; ok {
|
||||
json.Unmarshal(v, &shot.Duration)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &shot.CoverURI)
|
||||
}
|
||||
if v, ok := raw["preview_uri"]; ok {
|
||||
json.Unmarshal(v, &shot.PreviewURI)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &shot.Available)
|
||||
}
|
||||
if v, ok := raw["views"]; ok {
|
||||
json.Unmarshal(v, &shot.Views)
|
||||
}
|
||||
if v, ok := raw["likes"]; ok {
|
||||
json.Unmarshal(v, &shot.Likes)
|
||||
}
|
||||
if v, ok := raw["comments"]; ok {
|
||||
json.Unmarshal(v, &shot.Comments)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &shot.CreatedAt)
|
||||
}
|
||||
if v, ok := raw["shot_type"]; ok {
|
||||
json.Unmarshal(v, &shot.ShotType)
|
||||
}
|
||||
if v, ok := raw["tags"]; ok {
|
||||
json.Unmarshal(v, &shot.Tags)
|
||||
}
|
||||
if v, ok := raw["is_vertical"]; ok {
|
||||
json.Unmarshal(v, &shot.IsVertical)
|
||||
}
|
||||
if v, ok := raw["aspect_ratio"]; ok {
|
||||
json.Unmarshal(v, &shot.AspectRatio)
|
||||
}
|
||||
|
||||
if v, ok := raw["author"]; ok {
|
||||
var user User
|
||||
json.Unmarshal(v, &user)
|
||||
shot.Author = &user
|
||||
}
|
||||
if v, ok := raw["track"]; ok {
|
||||
var track Track
|
||||
json.Unmarshal(v, &track)
|
||||
shot.Track = &track
|
||||
}
|
||||
|
||||
return shot, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Shots
|
||||
func (s *Shot) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
shot := &Shot{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, shot); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, shot)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ShotData represents shot data
|
||||
type ShotData struct {
|
||||
BaseModel
|
||||
Shots []Shot `json:"shots"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ShotEvent represents a shot event
|
||||
type ShotEvent struct {
|
||||
BaseModel
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
ShotID string `json:"shot_id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// ShotType represents a shot type
|
||||
type ShotType struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Concert represents a concert
|
||||
type Concert struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Date string `json:"date"`
|
||||
Time string `json:"time"`
|
||||
Venue *ConcertVenue `json:"venue"`
|
||||
Location *ConcertLocation `json:"location"`
|
||||
MinPrice int `json:"min_price"`
|
||||
MaxPrice int `json:"max_price"`
|
||||
Currency string `json:"currency"`
|
||||
TicketURI string `json:"ticket_uri"`
|
||||
ImageURI string `json:"image_uri"`
|
||||
Available bool `json:"available"`
|
||||
SoldOut bool `json:"sold_out"`
|
||||
Artists []Artist `json:"artists"`
|
||||
Genres []string `json:"genres"`
|
||||
Cashback *ConcertCashback `json:"cashback"`
|
||||
Tabs []ConcertTabConfig `json:"tabs"`
|
||||
Skeleton *ConcertSkeleton `json:"skeleton"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Concert from JSON
|
||||
func (c *Concert) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
concert := &Concert{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &concert.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &concert.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &concert.Description)
|
||||
}
|
||||
if v, ok := raw["date"]; ok {
|
||||
json.Unmarshal(v, &concert.Date)
|
||||
}
|
||||
if v, ok := raw["time"]; ok {
|
||||
json.Unmarshal(v, &concert.Time)
|
||||
}
|
||||
if v, ok := raw["min_price"]; ok {
|
||||
json.Unmarshal(v, &concert.MinPrice)
|
||||
}
|
||||
if v, ok := raw["max_price"]; ok {
|
||||
json.Unmarshal(v, &concert.MaxPrice)
|
||||
}
|
||||
if v, ok := raw["currency"]; ok {
|
||||
json.Unmarshal(v, &concert.Currency)
|
||||
}
|
||||
if v, ok := raw["ticket_uri"]; ok {
|
||||
json.Unmarshal(v, &concert.TicketURI)
|
||||
}
|
||||
if v, ok := raw["image_uri"]; ok {
|
||||
json.Unmarshal(v, &concert.ImageURI)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &concert.Available)
|
||||
}
|
||||
if v, ok := raw["sold_out"]; ok {
|
||||
json.Unmarshal(v, &concert.SoldOut)
|
||||
}
|
||||
if v, ok := raw["genres"]; ok {
|
||||
json.Unmarshal(v, &concert.Genres)
|
||||
}
|
||||
|
||||
if v, ok := raw["venue"]; ok {
|
||||
var venue ConcertVenue
|
||||
json.Unmarshal(v, &venue)
|
||||
concert.Venue = &venue
|
||||
}
|
||||
if v, ok := raw["location"]; ok {
|
||||
var location ConcertLocation
|
||||
json.Unmarshal(v, &location)
|
||||
concert.Location = &location
|
||||
}
|
||||
if v, ok := raw["artists"]; ok {
|
||||
var artists []Artist
|
||||
json.Unmarshal(v, &artists)
|
||||
concert.Artists = artists
|
||||
}
|
||||
if v, ok := raw["cashback"]; ok {
|
||||
var cashback ConcertCashback
|
||||
json.Unmarshal(v, &cashback)
|
||||
concert.Cashback = &cashback
|
||||
}
|
||||
if v, ok := raw["tabs"]; ok {
|
||||
var tabs []ConcertTabConfig
|
||||
json.Unmarshal(v, &tabs)
|
||||
concert.Tabs = tabs
|
||||
}
|
||||
if v, ok := raw["skeleton"]; ok {
|
||||
var skeleton ConcertSkeleton
|
||||
json.Unmarshal(v, &skeleton)
|
||||
concert.Skeleton = &skeleton
|
||||
}
|
||||
|
||||
return concert, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Concerts
|
||||
func (c *Concert) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
concert := &Concert{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, concert); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, concert)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ConcertVenue represents a concert venue
|
||||
type ConcertVenue struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Capacity int `json:"capacity"`
|
||||
ImageURI string `json:"image_uri"`
|
||||
}
|
||||
|
||||
// ConcertLocation represents a concert location
|
||||
type ConcertLocation struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Timezone string `json:"timezone"`
|
||||
Venues []ConcertVenue `json:"venues"`
|
||||
}
|
||||
|
||||
// ConcertLocations represents concert locations
|
||||
type ConcertLocations struct {
|
||||
BaseModel
|
||||
Locations []ConcertLocation `json:"locations"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ConcertFeed represents a concert feed
|
||||
type ConcertFeed struct {
|
||||
BaseModel
|
||||
Concerts []Concert `json:"concerts"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ConcertInfo represents concert info
|
||||
type ConcertInfo struct {
|
||||
BaseModel
|
||||
Concert *Concert `json:"concert"`
|
||||
Related []Concert `json:"related"`
|
||||
}
|
||||
|
||||
// ConcertEventInfo represents concert event info
|
||||
type ConcertEventInfo struct {
|
||||
BaseModel
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// ConcertDescription represents concert description
|
||||
type ConcertDescription struct {
|
||||
BaseModel
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html"`
|
||||
ShortText string `json:"short_text"`
|
||||
}
|
||||
|
||||
// ConcertCashback represents concert cashback
|
||||
type ConcertCashback struct {
|
||||
BaseModel
|
||||
Amount int `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Percent int `json:"percent"`
|
||||
Expires string `json:"expires"`
|
||||
}
|
||||
|
||||
// ConcertMinPrice represents concert min price
|
||||
type ConcertMinPrice struct {
|
||||
BaseModel
|
||||
Price int `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// ConcertTabConfig represents concert tab config
|
||||
type ConcertTabConfig struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Active bool `json:"active"`
|
||||
Range *ConcertTabRange `json:"range"`
|
||||
}
|
||||
|
||||
// ConcertTabRange represents concert tab range
|
||||
type ConcertTabRange struct {
|
||||
BaseModel
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
// ConcertSkeleton represents concert skeleton
|
||||
type ConcertSkeleton struct {
|
||||
BaseModel
|
||||
Blocks []ConcertSkeletonBlock `json:"blocks"`
|
||||
}
|
||||
|
||||
// ConcertSkeletonBlock represents a concert skeleton block
|
||||
type ConcertSkeletonBlock struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Credits represents track credits
|
||||
type Credits struct {
|
||||
BaseModel
|
||||
Items []Credit `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes Credits from JSON
|
||||
func (c *Credits) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credits := &Credits{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["items"]; ok {
|
||||
var items []Credit
|
||||
json.Unmarshal(v, &items)
|
||||
credits.Items = items
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &credits.Total)
|
||||
}
|
||||
|
||||
return credits, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Credits
|
||||
func (c *Credits) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
credits := &Credits{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, credits); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, credits)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Credit represents a credit
|
||||
type Credit struct {
|
||||
BaseModel
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
ArtistID string `json:"artist_id"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
IsMain bool `json:"is_main"`
|
||||
TrackID string `json:"track_id"`
|
||||
AlbumID string `json:"album_id"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Credit from JSON
|
||||
func (c *Credit) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credit := &Credit{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["role"]; ok {
|
||||
json.Unmarshal(v, &credit.Role)
|
||||
}
|
||||
if v, ok := raw["name"]; ok {
|
||||
json.Unmarshal(v, &credit.Name)
|
||||
}
|
||||
if v, ok := raw["artist_id"]; ok {
|
||||
json.Unmarshal(v, &credit.ArtistID)
|
||||
}
|
||||
if v, ok := raw["artist_name"]; ok {
|
||||
json.Unmarshal(v, &credit.ArtistName)
|
||||
}
|
||||
if v, ok := raw["is_main"]; ok {
|
||||
json.Unmarshal(v, &credit.IsMain)
|
||||
}
|
||||
if v, ok := raw["track_id"]; ok {
|
||||
json.Unmarshal(v, &credit.TrackID)
|
||||
}
|
||||
if v, ok := raw["album_id"]; ok {
|
||||
json.Unmarshal(v, &credit.AlbumID)
|
||||
}
|
||||
|
||||
return credit, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Credits
|
||||
func (c *Credit) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
credit := &Credit{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, credit); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, credit)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LicenceTextPart represents a licence text part
|
||||
type LicenceTextPart struct {
|
||||
BaseModel
|
||||
Text string `json:"text"`
|
||||
Language string `json:"language"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
IsMain bool `json:"is_main"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a LicenceTextPart from JSON
|
||||
func (ltp *LicenceTextPart) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
part := &LicenceTextPart{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["text"]; ok {
|
||||
json.Unmarshal(v, &part.Text)
|
||||
}
|
||||
if v, ok := raw["language"]; ok {
|
||||
json.Unmarshal(v, &part.Language)
|
||||
}
|
||||
if v, ok := raw["url"]; ok {
|
||||
json.Unmarshal(v, &part.URL)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &part.Title)
|
||||
}
|
||||
if v, ok := raw["is_main"]; ok {
|
||||
json.Unmarshal(v, &part.IsMain)
|
||||
}
|
||||
|
||||
return part, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of LicenceTextPart
|
||||
func (ltp *LicenceTextPart) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
part := &LicenceTextPart{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, part); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, part)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Feed represents the main feed
|
||||
type Feed struct {
|
||||
BaseModel
|
||||
Days []Day `json:"days"`
|
||||
GeneratedPlaylists []GeneratedPlaylist `json:"generated_playlists"`
|
||||
PlayContexts []PlayContext `json:"play_contexts"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Feed from JSON
|
||||
func (f *Feed) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feed := &Feed{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["days"]; ok {
|
||||
var days []Day
|
||||
json.Unmarshal(v, &days)
|
||||
feed.Days = days
|
||||
}
|
||||
if v, ok := raw["generated_playlists"]; ok {
|
||||
var playlists []GeneratedPlaylist
|
||||
json.Unmarshal(v, &playlists)
|
||||
feed.GeneratedPlaylists = playlists
|
||||
}
|
||||
if v, ok := raw["play_contexts"]; ok {
|
||||
var contexts []PlayContext
|
||||
json.Unmarshal(v, &contexts)
|
||||
feed.PlayContexts = contexts
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Feeds
|
||||
func (f *Feed) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
feed := &Feed{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, feed); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, feed)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Day represents a day in the feed
|
||||
type Day struct {
|
||||
BaseModel
|
||||
Date string `json:"date"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Blocks []DayBlock `json:"blocks"`
|
||||
GeneratedPlaylists []GeneratedPlaylist `json:"generated_playlists"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Day from JSON
|
||||
func (d *Day) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
day := &Day{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["date"]; ok {
|
||||
json.Unmarshal(v, &day.Date)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &day.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &day.Description)
|
||||
}
|
||||
if v, ok := raw["blocks"]; ok {
|
||||
var blocks []DayBlock
|
||||
json.Unmarshal(v, &blocks)
|
||||
day.Blocks = blocks
|
||||
}
|
||||
if v, ok := raw["generated_playlists"]; ok {
|
||||
var playlists []GeneratedPlaylist
|
||||
json.Unmarshal(v, &playlists)
|
||||
day.GeneratedPlaylists = playlists
|
||||
}
|
||||
|
||||
return day, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Days
|
||||
func (d *Day) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
day := &Day{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, day); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, day)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DayBlock represents a block in a day
|
||||
type DayBlock struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Data *DayBlockData `json:"data"`
|
||||
}
|
||||
|
||||
// DayBlockData represents data in a day block
|
||||
type DayBlockData struct {
|
||||
Tracks []Track `json:"tracks"`
|
||||
Playlists []Playlist `json:"playlists"`
|
||||
Albums []Album `json:"albums"`
|
||||
Artists []Artist `json:"artists"`
|
||||
GeneratedPlaylist *GeneratedPlaylist `json:"generated_playlist"`
|
||||
}
|
||||
|
||||
// GeneratedPlaylist represents a generated playlist
|
||||
type GeneratedPlaylist struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
TrackCount int `json:"track_count"`
|
||||
DurationMs int `json:"duration_ms"`
|
||||
Kind int `json:"kind"`
|
||||
Revision int `json:"revision"`
|
||||
Type string `json:"type"`
|
||||
Available bool `json:"available"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a GeneratedPlaylist from JSON
|
||||
func (gp *GeneratedPlaylist) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlist := &GeneratedPlaylist{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &playlist.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &playlist.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &playlist.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &playlist.CoverURI)
|
||||
}
|
||||
if v, ok := raw["track_count"]; ok {
|
||||
json.Unmarshal(v, &playlist.TrackCount)
|
||||
}
|
||||
if v, ok := raw["duration_ms"]; ok {
|
||||
json.Unmarshal(v, &playlist.DurationMs)
|
||||
}
|
||||
if v, ok := raw["kind"]; ok {
|
||||
json.Unmarshal(v, &playlist.Kind)
|
||||
}
|
||||
if v, ok := raw["revision"]; ok {
|
||||
json.Unmarshal(v, &playlist.Revision)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &playlist.Type)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &playlist.Available)
|
||||
}
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []Track
|
||||
json.Unmarshal(v, &tracks)
|
||||
playlist.Tracks = tracks
|
||||
}
|
||||
|
||||
return playlist, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of GeneratedPlaylists
|
||||
func (gp *GeneratedPlaylist) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
playlist := &GeneratedPlaylist{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, playlist); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, playlist)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Event represents a feed event
|
||||
type Event struct {
|
||||
BaseModel
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
TrackID string `json:"track_id"`
|
||||
AlbumID string `json:"album_id"`
|
||||
ArtistID string `json:"artist_id"`
|
||||
PlaylistID string `json:"playlist_id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// AlbumEvent represents an album event
|
||||
type AlbumEvent struct {
|
||||
BaseModel
|
||||
Album *Album `json:"album"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// ArtistEvent represents an artist event
|
||||
type ArtistEvent struct {
|
||||
BaseModel
|
||||
Artist *Artist `json:"artist"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// TrackWithAds represents a track with ads
|
||||
type TrackWithAds struct {
|
||||
BaseModel
|
||||
Track *Track `json:"track"`
|
||||
Ads []Ad `json:"ads"`
|
||||
AdParams *AdParams `json:"ad_params"`
|
||||
}
|
||||
|
||||
// Ad represents an ad
|
||||
type Ad struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
URL string `json:"url"`
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
// AdParams represents ad parameters
|
||||
type AdParams struct {
|
||||
BaseModel
|
||||
Frequency int `json:"frequency"`
|
||||
MaxPerDay int `json:"max_per_day"`
|
||||
Targeting string `json:"targeting"`
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Label represents a label
|
||||
type Label struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Description string `json:"description"`
|
||||
Website string `json:"website"`
|
||||
SocialLinks []SocialLink `json:"social_links"`
|
||||
Available bool `json:"available"`
|
||||
Owner *User `json:"owner"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Label from JSON
|
||||
func (l *Label) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
label := &Label{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &label.ID)
|
||||
}
|
||||
if v, ok := raw["name"]; ok {
|
||||
json.Unmarshal(v, &label.Name)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &label.CoverURI)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &label.Description)
|
||||
}
|
||||
if v, ok := raw["website"]; ok {
|
||||
json.Unmarshal(v, &label.Website)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &label.Available)
|
||||
}
|
||||
if v, ok := raw["social_links"]; ok {
|
||||
var links []SocialLink
|
||||
json.Unmarshal(v, &links)
|
||||
label.SocialLinks = links
|
||||
}
|
||||
if v, ok := raw["owner"]; ok {
|
||||
var user User
|
||||
json.Unmarshal(v, &user)
|
||||
label.Owner = &user
|
||||
}
|
||||
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Labels
|
||||
func (l *Label) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
label := &Label{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, label); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, label)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LabelAlbums represents label albums
|
||||
type LabelAlbums struct {
|
||||
BaseModel
|
||||
Albums []Album `json:"albums"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// LabelArtists represents label artists
|
||||
type LabelArtists struct {
|
||||
BaseModel
|
||||
Artists []Artist `json:"artists"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Landing represents a landing page
|
||||
type Landing struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Type string `json:"type"`
|
||||
Blocks []LandingBlock `json:"blocks"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Landing from JSON
|
||||
func (l *Landing) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
landing := &Landing{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &landing.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &landing.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &landing.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &landing.CoverURI)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &landing.Type)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &landing.Available)
|
||||
}
|
||||
|
||||
if v, ok := raw["blocks"]; ok {
|
||||
var blocks []LandingBlock
|
||||
json.Unmarshal(v, &blocks)
|
||||
landing.Blocks = blocks
|
||||
}
|
||||
|
||||
return landing, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Landings
|
||||
func (l *Landing) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
landing := &Landing{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, landing); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, landing)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LandingBlock represents a block in a landing
|
||||
type LandingBlock struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Data *LandingBlockData `json:"data"`
|
||||
}
|
||||
|
||||
// LandingBlockData represents data in a landing block
|
||||
type LandingBlockData struct {
|
||||
Tracks []Track `json:"tracks"`
|
||||
Playlists []Playlist `json:"playlists"`
|
||||
Albums []Album `json:"albums"`
|
||||
Artists []Artist `json:"artists"`
|
||||
GeneratedPlaylist *GeneratedPlaylist `json:"generated_playlist"`
|
||||
}
|
||||
|
||||
// LandingList represents a list of landings
|
||||
type LandingList struct {
|
||||
BaseModel
|
||||
Landings []Landing `json:"landings"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a LandingList from JSON
|
||||
func (ll *LandingList) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := &LandingList{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["landings"]; ok {
|
||||
var landings []Landing
|
||||
json.Unmarshal(v, &landings)
|
||||
list.Landings = landings
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &list.Total)
|
||||
}
|
||||
if v, ok := raw["page"]; ok {
|
||||
json.Unmarshal(v, &list.Page)
|
||||
}
|
||||
if v, ok := raw["page_size"]; ok {
|
||||
json.Unmarshal(v, &list.PageSize)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of LandingLists
|
||||
func (ll *LandingList) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
list := &LandingList{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, list); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, list)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Metatag represents a metatag
|
||||
type Metatag struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Type string `json:"type"`
|
||||
Position int `json:"position"`
|
||||
Children []Metatag `json:"children"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Metatag from JSON
|
||||
func (m *Metatag) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metatag := &Metatag{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &metatag.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &metatag.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &metatag.Description)
|
||||
}
|
||||
if v, ok := raw["image"]; ok {
|
||||
json.Unmarshal(v, &metatag.Image)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &metatag.Type)
|
||||
}
|
||||
if v, ok := raw["position"]; ok {
|
||||
json.Unmarshal(v, &metatag.Position)
|
||||
}
|
||||
if v, ok := raw["children"]; ok {
|
||||
var children []Metatag
|
||||
json.Unmarshal(v, &children)
|
||||
metatag.Children = children
|
||||
}
|
||||
|
||||
return metatag, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Metatags
|
||||
func (m *Metatag) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
metatag := &Metatag{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, metatag); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, metatag)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MetatagTree represents a metatag tree
|
||||
type MetatagTree struct {
|
||||
BaseModel
|
||||
Root *Metatag `json:"root"`
|
||||
Leaves []Metatag `json:"leaves"`
|
||||
}
|
||||
|
||||
// MetatagLeaf represents a metatag leaf
|
||||
type MetatagLeaf struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Type string `json:"type"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
// MetatagArtists represents metatag artists
|
||||
type MetatagArtists struct {
|
||||
BaseModel
|
||||
Artists []Artist `json:"artists"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MetatagAlbums represents metatag albums
|
||||
type MetatagAlbums struct {
|
||||
BaseModel
|
||||
Albums []Album `json:"albums"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MetatagPlaylists represents metatag playlists
|
||||
type MetatagPlaylists struct {
|
||||
BaseModel
|
||||
Playlists []Playlist `json:"playlists"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MetatagTitle represents a metatag title
|
||||
type MetatagTitle struct {
|
||||
BaseModel
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// MetatagSortByValue represents metatag sort by value
|
||||
type MetatagSortByValue struct {
|
||||
BaseModel
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// MetatagArtistEntry represents a metatag artist entry
|
||||
type MetatagArtistEntry struct {
|
||||
BaseModel
|
||||
Artist *Artist `json:"artist"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
// Genre represents a genre
|
||||
type Genre struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Position int `json:"position"`
|
||||
ParentID string `json:"parent_id"`
|
||||
Children []Genre `json:"children"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Genre from JSON
|
||||
func (g *Genre) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
genre := &Genre{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &genre.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &genre.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &genre.Description)
|
||||
}
|
||||
if v, ok := raw["image"]; ok {
|
||||
json.Unmarshal(v, &genre.Image)
|
||||
}
|
||||
if v, ok := raw["position"]; ok {
|
||||
json.Unmarshal(v, &genre.Position)
|
||||
}
|
||||
if v, ok := raw["parent_id"]; ok {
|
||||
json.Unmarshal(v, &genre.ParentID)
|
||||
}
|
||||
if v, ok := raw["children"]; ok {
|
||||
var children []Genre
|
||||
json.Unmarshal(v, &children)
|
||||
genre.Children = children
|
||||
}
|
||||
|
||||
return genre, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Genres
|
||||
func (g *Genre) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
genre := &Genre{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, genre); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, genre)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Chart represents a chart
|
||||
type Chart struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Type string `json:"type"`
|
||||
Items []ChartItem `json:"items"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Chart from JSON
|
||||
func (c *Chart) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chart := &Chart{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &chart.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &chart.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &chart.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &chart.CoverURI)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &chart.Type)
|
||||
}
|
||||
if v, ok := raw["items"]; ok {
|
||||
var items []ChartItem
|
||||
json.Unmarshal(v, &items)
|
||||
chart.Items = items
|
||||
}
|
||||
|
||||
return chart, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Charts
|
||||
func (c *Chart) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
chart := &Chart{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, chart); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, chart)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ChartInfo represents chart info
|
||||
type ChartInfo struct {
|
||||
BaseModel
|
||||
Chart *Chart `json:"chart"`
|
||||
Menu []ChartInfoMenu `json:"menu"`
|
||||
}
|
||||
|
||||
// ChartItem represents a chart item
|
||||
type ChartItem struct {
|
||||
BaseModel
|
||||
Position int `json:"position"`
|
||||
Track *Track `json:"track"`
|
||||
Artist *Artist `json:"artist"`
|
||||
Album *Album `json:"album"`
|
||||
Playlist *Playlist `json:"playlist"`
|
||||
Score float64 `json:"score"`
|
||||
Trend string `json:"trend"`
|
||||
}
|
||||
|
||||
// ChartInfoMenu represents a chart info menu
|
||||
type ChartInfoMenu struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Items []ChartInfoMenuItem `json:"items"`
|
||||
}
|
||||
|
||||
// ChartInfoMenuItem represents a chart info menu item
|
||||
type ChartInfoMenuItem struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// ChartItems represents chart items response
|
||||
type ChartItems struct {
|
||||
BaseModel
|
||||
Items []ChartItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// MusicHistory represents music history
|
||||
type MusicHistory struct {
|
||||
BaseModel
|
||||
Tabs []MusicHistoryTab `json:"tabs"`
|
||||
Groups []MusicHistoryGroup `json:"groups"`
|
||||
Items []MusicHistoryItem `json:"items"`
|
||||
Context *MusicHistoryContextFullModel `json:"context"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a MusicHistory from JSON
|
||||
func (mh *MusicHistory) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
history := &MusicHistory{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["tabs"]; ok {
|
||||
var tabs []MusicHistoryTab
|
||||
json.Unmarshal(v, &tabs)
|
||||
history.Tabs = tabs
|
||||
}
|
||||
if v, ok := raw["groups"]; ok {
|
||||
var groups []MusicHistoryGroup
|
||||
json.Unmarshal(v, &groups)
|
||||
history.Groups = groups
|
||||
}
|
||||
if v, ok := raw["items"]; ok {
|
||||
var items []MusicHistoryItem
|
||||
json.Unmarshal(v, &items)
|
||||
history.Items = items
|
||||
}
|
||||
if v, ok := raw["context"]; ok {
|
||||
var context MusicHistoryContextFullModel
|
||||
json.Unmarshal(v, &context)
|
||||
history.Context = &context
|
||||
}
|
||||
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of MusicHistory
|
||||
func (mh *MusicHistory) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
history := &MusicHistory{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, history); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, history)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MusicHistoryTab represents a music history tab
|
||||
type MusicHistoryTab struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
Active bool `json:"active"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
// MusicHistoryGroup represents a music history group
|
||||
type MusicHistoryGroup struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Items []MusicHistoryItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// MusicHistoryItem represents a music history item
|
||||
type MusicHistoryItem struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "track", "album", "artist", "playlist"
|
||||
Track *Track `json:"track"`
|
||||
Album *Album `json:"album"`
|
||||
Artist *Artist `json:"artist"`
|
||||
Playlist *Playlist `json:"playlist"`
|
||||
PlayedAt string `json:"played_at"`
|
||||
PlayCount int `json:"play_count"`
|
||||
Duration int `json:"duration"`
|
||||
Context *MusicHistoryItemData `json:"context"`
|
||||
}
|
||||
|
||||
// MusicHistoryItemData represents music history item data
|
||||
type MusicHistoryItemData struct {
|
||||
BaseModel
|
||||
Source string `json:"source"`
|
||||
SourceID string `json:"source_id"`
|
||||
SourceType string `json:"source_type"`
|
||||
PlayContext *PlayContext `json:"play_context"`
|
||||
Position int `json:"position"`
|
||||
Duration int `json:"duration"`
|
||||
Progress float64 `json:"progress"`
|
||||
}
|
||||
|
||||
// MusicHistoryItemID represents music history item ID
|
||||
type MusicHistoryItemID struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// MusicHistoryItems represents music history items
|
||||
type MusicHistoryItems struct {
|
||||
BaseModel
|
||||
Items []MusicHistoryItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MusicHistoryContextFullModel represents music history context full model
|
||||
type MusicHistoryContextFullModel struct {
|
||||
BaseModel
|
||||
DeviceID string `json:"device_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
ClientInfo string `json:"client_info"`
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Pin represents a pin
|
||||
type Pin struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Type string `json:"type"` // "track", "album", "artist", "playlist"
|
||||
Track *Track `json:"track"`
|
||||
Album *Album `json:"album"`
|
||||
Artist *Artist `json:"artist"`
|
||||
Playlist *Playlist `json:"playlist"`
|
||||
Position int `json:"position"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Pin from JSON
|
||||
func (p *Pin) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pin := &Pin{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &pin.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &pin.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &pin.Description)
|
||||
}
|
||||
if v, ok := raw["image"]; ok {
|
||||
json.Unmarshal(v, &pin.Image)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &pin.Type)
|
||||
}
|
||||
if v, ok := raw["position"]; ok {
|
||||
json.Unmarshal(v, &pin.Position)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &pin.CreatedAt)
|
||||
}
|
||||
if v, ok := raw["expires_at"]; ok {
|
||||
json.Unmarshal(v, &pin.ExpiresAt)
|
||||
}
|
||||
if v, ok := raw["active"]; ok {
|
||||
json.Unmarshal(v, &pin.Active)
|
||||
}
|
||||
|
||||
if v, ok := raw["track"]; ok {
|
||||
var track Track
|
||||
json.Unmarshal(v, &track)
|
||||
pin.Track = &track
|
||||
}
|
||||
if v, ok := raw["album"]; ok {
|
||||
var album Album
|
||||
json.Unmarshal(v, &album)
|
||||
pin.Album = &album
|
||||
}
|
||||
if v, ok := raw["artist"]; ok {
|
||||
var artist Artist
|
||||
json.Unmarshal(v, &artist)
|
||||
pin.Artist = &artist
|
||||
}
|
||||
if v, ok := raw["playlist"]; ok {
|
||||
var playlist Playlist
|
||||
json.Unmarshal(v, &playlist)
|
||||
pin.Playlist = &playlist
|
||||
}
|
||||
|
||||
return pin, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Pins
|
||||
func (p *Pin) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
pin := &Pin{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, pin); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, pin)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PinData represents pin data
|
||||
type PinData struct {
|
||||
BaseModel
|
||||
Pins []Pin `json:"pins"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// PinsList represents a list of pins
|
||||
type PinsList struct {
|
||||
BaseModel
|
||||
Pins []Pin `json:"pins"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// Presaves represents presaves
|
||||
type Presaves struct {
|
||||
BaseModel
|
||||
Presaves []Presave `json:"presaves"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// Presave represents a presave
|
||||
type Presave struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Track *Track `json:"track"`
|
||||
Album *Album `json:"album"`
|
||||
Artist *Artist `json:"artist"`
|
||||
Status string `json:"status"` // "pending", "released", "cancelled"
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Presave from JSON
|
||||
func (p *Presave) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
presave := &Presave{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &presave.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &presave.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &presave.Description)
|
||||
}
|
||||
if v, ok := raw["image"]; ok {
|
||||
json.Unmarshal(v, &presave.Image)
|
||||
}
|
||||
if v, ok := raw["release_date"]; ok {
|
||||
json.Unmarshal(v, &presave.ReleaseDate)
|
||||
}
|
||||
if v, ok := raw["status"]; ok {
|
||||
json.Unmarshal(v, &presave.Status)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &presave.CreatedAt)
|
||||
}
|
||||
if v, ok := raw["updated_at"]; ok {
|
||||
json.Unmarshal(v, &presave.UpdatedAt)
|
||||
}
|
||||
|
||||
if v, ok := raw["track"]; ok {
|
||||
var track Track
|
||||
json.Unmarshal(v, &track)
|
||||
presave.Track = &track
|
||||
}
|
||||
if v, ok := raw["album"]; ok {
|
||||
var album Album
|
||||
json.Unmarshal(v, &album)
|
||||
presave.Album = &album
|
||||
}
|
||||
if v, ok := raw["artist"]; ok {
|
||||
var artist Artist
|
||||
json.Unmarshal(v, &artist)
|
||||
presave.Artist = &artist
|
||||
}
|
||||
|
||||
return presave, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Presaves
|
||||
func (p *Presave) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
presave := &Presave{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, presave); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, presave)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Playlist represents a playlist
|
||||
type Playlist struct {
|
||||
BaseModel
|
||||
Kind int `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Created string `json:"created"`
|
||||
Modified string `json:"modified"`
|
||||
TrackCount int `json:"track_count"`
|
||||
DurationMs int `json:"duration_ms"`
|
||||
Availability *Availability `json:"availability"`
|
||||
Owner *User `json:"owner"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
LikesCount int `json:"likes_count"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
Revision int `json:"revision"`
|
||||
UUID string `json:"uuid"`
|
||||
Tags []string `json:"tags"`
|
||||
SimilarPlaylists []Playlist `json:"similar_playlists"`
|
||||
// Additional fields
|
||||
AccessType string `json:"access_type"`
|
||||
Available bool `json:"available"`
|
||||
ContentWarning string `json:"content_warning"`
|
||||
Genre string `json:"genre"`
|
||||
Image []Image `json:"image"`
|
||||
OwnerUID int64 `json:"owner_uid"`
|
||||
Shared bool `json:"shared"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
Trailer *TrailerInfo `json:"trailer"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Playlist from JSON
|
||||
func (p *Playlist) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlist := &Playlist{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
// Simple fields
|
||||
if v, ok := raw["kind"]; ok {
|
||||
json.Unmarshal(v, &playlist.Kind)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &playlist.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &playlist.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &playlist.CoverURI)
|
||||
}
|
||||
if v, ok := raw["created"]; ok {
|
||||
json.Unmarshal(v, &playlist.Created)
|
||||
}
|
||||
if v, ok := raw["modified"]; ok {
|
||||
json.Unmarshal(v, &playlist.Modified)
|
||||
}
|
||||
if v, ok := raw["track_count"]; ok {
|
||||
json.Unmarshal(v, &playlist.TrackCount)
|
||||
}
|
||||
if v, ok := raw["duration_ms"]; ok {
|
||||
json.Unmarshal(v, &playlist.DurationMs)
|
||||
}
|
||||
if v, ok := raw["likes_count"]; ok {
|
||||
json.Unmarshal(v, &playlist.LikesCount)
|
||||
}
|
||||
if v, ok := raw["is_public"]; ok {
|
||||
json.Unmarshal(v, &playlist.IsPublic)
|
||||
}
|
||||
if v, ok := raw["revision"]; ok {
|
||||
json.Unmarshal(v, &playlist.Revision)
|
||||
}
|
||||
if v, ok := raw["uuid"]; ok {
|
||||
json.Unmarshal(v, &playlist.UUID)
|
||||
}
|
||||
if v, ok := raw["tags"]; ok {
|
||||
json.Unmarshal(v, &playlist.Tags)
|
||||
}
|
||||
if v, ok := raw["access_type"]; ok {
|
||||
json.Unmarshal(v, &playlist.AccessType)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &playlist.Available)
|
||||
}
|
||||
if v, ok := raw["content_warning"]; ok {
|
||||
json.Unmarshal(v, &playlist.ContentWarning)
|
||||
}
|
||||
if v, ok := raw["genre"]; ok {
|
||||
json.Unmarshal(v, &playlist.Genre)
|
||||
}
|
||||
if v, ok := raw["owner_uid"]; ok {
|
||||
json.Unmarshal(v, &playlist.OwnerUID)
|
||||
}
|
||||
if v, ok := raw["shared"]; ok {
|
||||
json.Unmarshal(v, &playlist.Shared)
|
||||
}
|
||||
if v, ok := raw["snapshot_id"]; ok {
|
||||
json.Unmarshal(v, &playlist.SnapshotID)
|
||||
}
|
||||
|
||||
// Nested objects
|
||||
if v, ok := raw["availability"]; ok {
|
||||
var avail Availability
|
||||
json.Unmarshal(v, &avail)
|
||||
playlist.Availability = &avail
|
||||
}
|
||||
if v, ok := raw["owner"]; ok {
|
||||
var user User
|
||||
json.Unmarshal(v, &user)
|
||||
playlist.Owner = &user
|
||||
}
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []Track
|
||||
json.Unmarshal(v, &tracks)
|
||||
playlist.Tracks = tracks
|
||||
}
|
||||
if v, ok := raw["similar_playlists"]; ok {
|
||||
var similar []Playlist
|
||||
json.Unmarshal(v, &similar)
|
||||
playlist.SimilarPlaylists = similar
|
||||
}
|
||||
if v, ok := raw["image"]; ok {
|
||||
var images []Image
|
||||
json.Unmarshal(v, &images)
|
||||
playlist.Image = images
|
||||
}
|
||||
if v, ok := raw["trailer"]; ok {
|
||||
var trailer TrailerInfo
|
||||
json.Unmarshal(v, &trailer)
|
||||
playlist.Trailer = &trailer
|
||||
}
|
||||
|
||||
return playlist, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Playlists
|
||||
func (p *Playlist) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
playlist := &Playlist{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, playlist); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, playlist)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// User represents a user (playlist owner)
|
||||
type User struct {
|
||||
BaseModel
|
||||
UID int64 `json:"uid"`
|
||||
Login string `json:"login"`
|
||||
DisplayName string `json:"display_name"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Sex string `json:"sex"`
|
||||
PublicPlaylists int `json:"public_playlists"`
|
||||
FollowersCount int `json:"followers_count"`
|
||||
FollowingsCount int `json:"followings_count"`
|
||||
HasPlaylists bool `json:"has_playlists"`
|
||||
Available bool `json:"available"`
|
||||
// Additional fields
|
||||
Description string `json:"description"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Birthday string `json:"birthday"`
|
||||
RegistrationDate string `json:"registration_date"`
|
||||
Settings *UserSettings `json:"settings"`
|
||||
Counters *UserCounters `json:"counters"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a User from JSON
|
||||
func (u *User) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &User{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["uid"]; ok {
|
||||
json.Unmarshal(v, &user.UID)
|
||||
}
|
||||
if v, ok := raw["login"]; ok {
|
||||
json.Unmarshal(v, &user.Login)
|
||||
}
|
||||
if v, ok := raw["display_name"]; ok {
|
||||
json.Unmarshal(v, &user.DisplayName)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &user.CoverURI)
|
||||
}
|
||||
if v, ok := raw["sex"]; ok {
|
||||
json.Unmarshal(v, &user.Sex)
|
||||
}
|
||||
if v, ok := raw["public_playlists"]; ok {
|
||||
json.Unmarshal(v, &user.PublicPlaylists)
|
||||
}
|
||||
if v, ok := raw["followers_count"]; ok {
|
||||
json.Unmarshal(v, &user.FollowersCount)
|
||||
}
|
||||
if v, ok := raw["followings_count"]; ok {
|
||||
json.Unmarshal(v, &user.FollowingsCount)
|
||||
}
|
||||
if v, ok := raw["has_playlists"]; ok {
|
||||
json.Unmarshal(v, &user.HasPlaylists)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &user.Available)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &user.Description)
|
||||
}
|
||||
if v, ok := raw["is_verified"]; ok {
|
||||
json.Unmarshal(v, &user.IsVerified)
|
||||
}
|
||||
if v, ok := raw["city"]; ok {
|
||||
json.Unmarshal(v, &user.City)
|
||||
}
|
||||
if v, ok := raw["country"]; ok {
|
||||
json.Unmarshal(v, &user.Country)
|
||||
}
|
||||
if v, ok := raw["birthday"]; ok {
|
||||
json.Unmarshal(v, &user.Birthday)
|
||||
}
|
||||
if v, ok := raw["registration_date"]; ok {
|
||||
json.Unmarshal(v, &user.RegistrationDate)
|
||||
}
|
||||
if v, ok := raw["settings"]; ok {
|
||||
var settings UserSettings
|
||||
json.Unmarshal(v, &settings)
|
||||
user.Settings = &settings
|
||||
}
|
||||
if v, ok := raw["counters"]; ok {
|
||||
var counters UserCounters
|
||||
json.Unmarshal(v, &counters)
|
||||
user.Counters = &counters
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Users
|
||||
func (u *User) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
user := &User{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, user); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, user)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UserSettings represents user settings (from /account/settings)
|
||||
type UserSettings struct {
|
||||
BaseModel
|
||||
UID int64 `json:"uid"`
|
||||
LastFMScrobblingEnabled bool `json:"last_fm_scrobbling_enabled"`
|
||||
ShuffleEnabled bool `json:"shuffle_enabled"`
|
||||
VolumePercents int `json:"volume_percents"`
|
||||
Modified string `json:"modified"`
|
||||
FacebookScrobblingEnabled bool `json:"facebook_scrobbling_enabled"`
|
||||
AddNewTrackOnPlaylistTop bool `json:"add_new_track_on_playlist_top"`
|
||||
UserMusicVisibility string `json:"user_music_visibility"`
|
||||
UserSocialVisibility string `json:"user_social_visibility"`
|
||||
RBTDisabled bool `json:"rbt_disabled"`
|
||||
Theme string `json:"theme"`
|
||||
PromosDisabled bool `json:"promos_disabled"`
|
||||
AutoPlayRadio bool `json:"auto_play_radio"`
|
||||
SyncQueueEnabled bool `json:"sync_queue_enabled"`
|
||||
AdsDisabled *bool `json:"ads_disabled,omitempty"`
|
||||
DiskEnabled *bool `json:"disk_enabled,omitempty"`
|
||||
ShowDiskTracksInLibrary *bool `json:"show_disk_tracks_in_library,omitempty"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes UserSettings from JSON
|
||||
func (u *UserSettings) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings := &UserSettings{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["uid"]; ok {
|
||||
json.Unmarshal(v, &settings.UID)
|
||||
}
|
||||
if v, ok := raw["last_fm_scrobbling_enabled"]; ok {
|
||||
json.Unmarshal(v, &settings.LastFMScrobblingEnabled)
|
||||
}
|
||||
if v, ok := raw["shuffle_enabled"]; ok {
|
||||
json.Unmarshal(v, &settings.ShuffleEnabled)
|
||||
}
|
||||
if v, ok := raw["volume_percents"]; ok {
|
||||
json.Unmarshal(v, &settings.VolumePercents)
|
||||
}
|
||||
if v, ok := raw["modified"]; ok {
|
||||
json.Unmarshal(v, &settings.Modified)
|
||||
}
|
||||
if v, ok := raw["facebook_scrobbling_enabled"]; ok {
|
||||
json.Unmarshal(v, &settings.FacebookScrobblingEnabled)
|
||||
}
|
||||
if v, ok := raw["add_new_track_on_playlist_top"]; ok {
|
||||
json.Unmarshal(v, &settings.AddNewTrackOnPlaylistTop)
|
||||
}
|
||||
if v, ok := raw["user_music_visibility"]; ok {
|
||||
json.Unmarshal(v, &settings.UserMusicVisibility)
|
||||
}
|
||||
if v, ok := raw["user_social_visibility"]; ok {
|
||||
json.Unmarshal(v, &settings.UserSocialVisibility)
|
||||
}
|
||||
if v, ok := raw["rbt_disabled"]; ok {
|
||||
json.Unmarshal(v, &settings.RBTDisabled)
|
||||
}
|
||||
if v, ok := raw["theme"]; ok {
|
||||
json.Unmarshal(v, &settings.Theme)
|
||||
}
|
||||
if v, ok := raw["promos_disabled"]; ok {
|
||||
json.Unmarshal(v, &settings.PromosDisabled)
|
||||
}
|
||||
if v, ok := raw["auto_play_radio"]; ok {
|
||||
json.Unmarshal(v, &settings.AutoPlayRadio)
|
||||
}
|
||||
if v, ok := raw["sync_queue_enabled"]; ok {
|
||||
json.Unmarshal(v, &settings.SyncQueueEnabled)
|
||||
}
|
||||
if v, ok := raw["ads_disabled"]; ok {
|
||||
var val bool
|
||||
json.Unmarshal(v, &val)
|
||||
settings.AdsDisabled = &val
|
||||
}
|
||||
if v, ok := raw["disk_enabled"]; ok {
|
||||
var val bool
|
||||
json.Unmarshal(v, &val)
|
||||
settings.DiskEnabled = &val
|
||||
}
|
||||
if v, ok := raw["show_disk_tracks_in_library"]; ok {
|
||||
var val bool
|
||||
json.Unmarshal(v, &val)
|
||||
settings.ShowDiskTracksInLibrary = &val
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of UserSettings
|
||||
func (u *UserSettings) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
settings := &UserSettings{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, settings); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, settings)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UserCounters represents user counters
|
||||
type UserCounters struct {
|
||||
Tracks int `json:"tracks"`
|
||||
Albums int `json:"albums"`
|
||||
Playlists int `json:"playlists"`
|
||||
Followers int `json:"followers"`
|
||||
Followings int `json:"followings"`
|
||||
Likes int `json:"likes"`
|
||||
}
|
||||
|
||||
// Image represents an image with multiple sizes
|
||||
type Image struct {
|
||||
URI string `json:"uri"`
|
||||
Type string `json:"type"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Size int64 `json:"size"`
|
||||
Extension string `json:"extension"`
|
||||
}
|
||||
|
||||
// Re-export types from other files for convenience
|
||||
// Availability, TrailerInfo are defined in album.go
|
||||
@@ -0,0 +1,292 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Queue represents a playback queue
|
||||
type Queue struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
TrackIDs []string `json:"track_ids"`
|
||||
CurrentIndex int `json:"current_index"`
|
||||
QueueType string `json:"queue_type"`
|
||||
RepeatMode string `json:"repeat_mode"`
|
||||
Shuffle bool `json:"shuffle"`
|
||||
Device string `json:"device"`
|
||||
Items []QueueItem `json:"items"`
|
||||
Context *PlayContext `json:"context"`
|
||||
AvailableTracks int `json:"available_tracks"`
|
||||
TotalDuration int `json:"total_duration_ms"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Queue from JSON
|
||||
func (q *Queue) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queue := &Queue{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &queue.ID)
|
||||
}
|
||||
if v, ok := raw["track_ids"]; ok {
|
||||
json.Unmarshal(v, &queue.TrackIDs)
|
||||
}
|
||||
if v, ok := raw["current_index"]; ok {
|
||||
json.Unmarshal(v, &queue.CurrentIndex)
|
||||
}
|
||||
if v, ok := raw["queue_type"]; ok {
|
||||
json.Unmarshal(v, &queue.QueueType)
|
||||
}
|
||||
if v, ok := raw["repeat_mode"]; ok {
|
||||
json.Unmarshal(v, &queue.RepeatMode)
|
||||
}
|
||||
if v, ok := raw["shuffle"]; ok {
|
||||
json.Unmarshal(v, &queue.Shuffle)
|
||||
}
|
||||
if v, ok := raw["device"]; ok {
|
||||
json.Unmarshal(v, &queue.Device)
|
||||
}
|
||||
if v, ok := raw["available_tracks"]; ok {
|
||||
json.Unmarshal(v, &queue.AvailableTracks)
|
||||
}
|
||||
if v, ok := raw["total_duration_ms"]; ok {
|
||||
json.Unmarshal(v, &queue.TotalDuration)
|
||||
}
|
||||
|
||||
if v, ok := raw["items"]; ok {
|
||||
var items []QueueItem
|
||||
json.Unmarshal(v, &items)
|
||||
queue.Items = items
|
||||
}
|
||||
if v, ok := raw["context"]; ok {
|
||||
var ctx PlayContext
|
||||
json.Unmarshal(v, &ctx)
|
||||
queue.Context = &ctx
|
||||
}
|
||||
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Queues
|
||||
func (q *Queue) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
queue := &Queue{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, queue); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, queue)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// QueueItem represents a queue item
|
||||
type QueueItem struct {
|
||||
BaseModel
|
||||
TrackID string `json:"track_id"`
|
||||
Track *Track `json:"track"`
|
||||
Position int `json:"position"`
|
||||
AddedAt string `json:"added_at"`
|
||||
AddedBy string `json:"added_by"`
|
||||
Playable bool `json:"playable"`
|
||||
PlayContext *PlayContext `json:"play_context"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a QueueItem from JSON
|
||||
func (qi *QueueItem) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := &QueueItem{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["track_id"]; ok {
|
||||
json.Unmarshal(v, &item.TrackID)
|
||||
}
|
||||
if v, ok := raw["position"]; ok {
|
||||
json.Unmarshal(v, &item.Position)
|
||||
}
|
||||
if v, ok := raw["added_at"]; ok {
|
||||
json.Unmarshal(v, &item.AddedAt)
|
||||
}
|
||||
if v, ok := raw["added_by"]; ok {
|
||||
json.Unmarshal(v, &item.AddedBy)
|
||||
}
|
||||
if v, ok := raw["playable"]; ok {
|
||||
json.Unmarshal(v, &item.Playable)
|
||||
}
|
||||
|
||||
if v, ok := raw["track"]; ok {
|
||||
var track Track
|
||||
json.Unmarshal(v, &track)
|
||||
item.Track = &track
|
||||
}
|
||||
if v, ok := raw["play_context"]; ok {
|
||||
var ctx PlayContext
|
||||
json.Unmarshal(v, &ctx)
|
||||
item.PlayContext = &ctx
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of QueueItems
|
||||
func (qi *QueueItem) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
qi := &QueueItem{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, qi); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, qi)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PlayContext represents a play context
|
||||
type PlayContext struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "playlist", "album", "artist", "radio", "search"
|
||||
SourceID string `json:"source_id"`
|
||||
SourceKind string `json:"source_kind"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
TrackCount int `json:"track_count"`
|
||||
Owner *User `json:"owner"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a PlayContext from JSON
|
||||
func (pc *PlayContext) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := &PlayContext{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &ctx.ID)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &ctx.Type)
|
||||
}
|
||||
if v, ok := raw["source_id"]; ok {
|
||||
json.Unmarshal(v, &ctx.SourceID)
|
||||
}
|
||||
if v, ok := raw["source_kind"]; ok {
|
||||
json.Unmarshal(v, &ctx.SourceKind)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &ctx.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &ctx.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &ctx.CoverURI)
|
||||
}
|
||||
if v, ok := raw["track_count"]; ok {
|
||||
json.Unmarshal(v, &ctx.TrackCount)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &ctx.Available)
|
||||
}
|
||||
|
||||
if v, ok := raw["owner"]; ok {
|
||||
var user User
|
||||
json.Unmarshal(v, &user)
|
||||
ctx.Owner = &user
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of PlayContexts
|
||||
func (pc *PlayContext) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
ctx := &PlayContext{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, ctx)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PlayContextsData represents play contexts data
|
||||
type PlayContextsData struct {
|
||||
BaseModel
|
||||
Contexts []PlayContext `json:"contexts"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes PlayContextsData from JSON
|
||||
func (pcd *PlayContextsData) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &PlayContextsData{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["contexts"]; ok {
|
||||
var contexts []PlayContext
|
||||
json.Unmarshal(v, &contexts)
|
||||
result.Contexts = contexts
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &result.Total)
|
||||
}
|
||||
if v, ok := raw["page"]; ok {
|
||||
json.Unmarshal(v, &result.Page)
|
||||
}
|
||||
if v, ok := raw["page_size"]; ok {
|
||||
json.Unmarshal(v, &result.PageSize)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of PlayContextsData
|
||||
func (pcd *PlayContextsData) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
d := &PlayContextsData{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, d); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, d)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// RadioStation represents a radio station
|
||||
type RadioStation struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Genre string `json:"genre"`
|
||||
Available bool `json:"available"`
|
||||
IsPersonal bool `json:"is_personal"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a RadioStation from JSON
|
||||
func (r *RadioStation) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
station := &RadioStation{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &station.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &station.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &station.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &station.CoverURI)
|
||||
}
|
||||
if v, ok := raw["genre"]; ok {
|
||||
json.Unmarshal(v, &station.Genre)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &station.Available)
|
||||
}
|
||||
if v, ok := raw["is_personal"]; ok {
|
||||
json.Unmarshal(v, &station.IsPersonal)
|
||||
}
|
||||
|
||||
return station, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of RadioStations
|
||||
func (r *RadioStation) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
station := &RadioStation{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, station); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, station)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// StationTracksResult represents station tracks result
|
||||
type StationTracksResult struct {
|
||||
BaseModel
|
||||
Tracks []Track `json:"tracks"`
|
||||
Total int `json:"total"`
|
||||
Sequence string `json:"sequence"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes StationTracksResult from JSON
|
||||
func (s *StationTracksResult) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &StationTracksResult{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []Track
|
||||
json.Unmarshal(v, &tracks)
|
||||
result.Tracks = tracks
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &result.Total)
|
||||
}
|
||||
if v, ok := raw["sequence"]; ok {
|
||||
json.Unmarshal(v, &result.Sequence)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of StationTracksResult
|
||||
func (s *StationTracksResult) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
r := &StationTracksResult{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, r); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RotorStation represents a rotor (generative) station
|
||||
type RotorStation struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Genre string `json:"genre"`
|
||||
StationType string `json:"station_type"`
|
||||
Available bool `json:"available"`
|
||||
Settings *RotorSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a RotorStation from JSON
|
||||
func (r *RotorStation) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
station := &RotorStation{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &station.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &station.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &station.Description)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &station.CoverURI)
|
||||
}
|
||||
if v, ok := raw["genre"]; ok {
|
||||
json.Unmarshal(v, &station.Genre)
|
||||
}
|
||||
if v, ok := raw["station_type"]; ok {
|
||||
json.Unmarshal(v, &station.StationType)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &station.Available)
|
||||
}
|
||||
if v, ok := raw["settings"]; ok {
|
||||
var settings RotorSettings
|
||||
json.Unmarshal(v, &settings)
|
||||
station.Settings = &settings
|
||||
}
|
||||
|
||||
return station, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of RotorStations
|
||||
func (r *RotorStation) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
station := &RotorStation{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, station); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, station)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RotorSettings represents rotor station settings
|
||||
type RotorSettings struct {
|
||||
BaseModel
|
||||
Diversity float64 `json:"diversity"`
|
||||
Energy float64 `json:"energy"`
|
||||
Acousticness float64 `json:"acousticness"`
|
||||
Instrumentalness float64 `json:"instrumentalness"`
|
||||
Live float64 `json:"live"`
|
||||
Speechiness float64 `json:"speechiness"`
|
||||
Tempo float64 `json:"tempo"`
|
||||
Valence float64 `json:"valence"`
|
||||
}
|
||||
|
||||
// RotorStationTracksResult represents rotor station tracks result
|
||||
type RotorStationTracksResult struct {
|
||||
BaseModel
|
||||
Tracks []Track `json:"tracks"`
|
||||
Total int `json:"total"`
|
||||
Sequence string `json:"sequence"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes RotorStationTracksResult from JSON
|
||||
func (r *RotorStationTracksResult) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &RotorStationTracksResult{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []Track
|
||||
json.Unmarshal(v, &tracks)
|
||||
result.Tracks = tracks
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &result.Total)
|
||||
}
|
||||
if v, ok := raw["sequence"]; ok {
|
||||
json.Unmarshal(v, &result.Sequence)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of RotorStationTracksResult
|
||||
func (r *RotorStationTracksResult) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
r := &RotorStationTracksResult{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, r); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RotorSession represents a rotor session
|
||||
type RotorSession struct {
|
||||
BaseModel
|
||||
SessionID string `json:"session_id"`
|
||||
StationID string `json:"station_id"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
BatchID string `json:"batch_id"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a RotorSession from JSON
|
||||
func (r *RotorSession) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := &RotorSession{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["session_id"]; ok {
|
||||
json.Unmarshal(v, &session.SessionID)
|
||||
}
|
||||
if v, ok := raw["station_id"]; ok {
|
||||
json.Unmarshal(v, &session.StationID)
|
||||
}
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []Track
|
||||
json.Unmarshal(v, &tracks)
|
||||
session.Tracks = tracks
|
||||
}
|
||||
if v, ok := raw["batch_id"]; ok {
|
||||
json.Unmarshal(v, &session.BatchID)
|
||||
}
|
||||
if v, ok := raw["expires_at"]; ok {
|
||||
json.Unmarshal(v, &session.ExpiresAt)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &session.CreatedAt)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of RotorSessions
|
||||
func (r *RotorSession) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
session := &RotorSession{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, session); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, session)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SessionFeedback represents session feedback
|
||||
type SessionFeedback struct {
|
||||
BaseModel
|
||||
TrackID string `json:"track_id"`
|
||||
Feedback string `json:"feedback"` // "like", "dislike", "skip"
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// SessionFeedbacks represents multiple session feedbacks
|
||||
type SessionFeedbacks struct {
|
||||
BaseModel
|
||||
Feedbacks []SessionFeedback `json:"feedbacks"`
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package models
|
||||
|
||||
// SearchResult represents search results
|
||||
type SearchResult struct {
|
||||
Tracks *SearchTracksResult `json:"tracks"`
|
||||
Albums *SearchAlbumsResult `json:"albums"`
|
||||
Artists *SearchArtistsResult `json:"artists"`
|
||||
Playlists *SearchPlaylistsResult `json:"playlists"`
|
||||
Videos *SearchVideosResult `json:"videos"`
|
||||
Users *SearchUsersResult `json:"users"`
|
||||
}
|
||||
|
||||
// SearchTracksResult represents track search results
|
||||
type SearchTracksResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []Track `json:"results"`
|
||||
}
|
||||
|
||||
// SearchAlbumsResult represents album search results
|
||||
type SearchAlbumsResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []Album `json:"results"`
|
||||
}
|
||||
|
||||
// SearchArtistsResult represents artist search results
|
||||
type SearchArtistsResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []Artist `json:"results"`
|
||||
}
|
||||
|
||||
// SearchPlaylistsResult represents playlist search results
|
||||
type SearchPlaylistsResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []Playlist `json:"results"`
|
||||
}
|
||||
|
||||
// SearchVideosResult represents video search results
|
||||
type SearchVideosResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []Video `json:"results"`
|
||||
}
|
||||
|
||||
// SearchUsersResult represents user search results
|
||||
type SearchUsersResult struct {
|
||||
Total int `json:"total"`
|
||||
Results []User `json:"results"`
|
||||
}
|
||||
|
||||
// SearchBanner represents a search banner
|
||||
type SearchBanner struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
URL string `json:"url"`
|
||||
Action string `json:"action"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Tag represents a search tag
|
||||
type Tag struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Count int `json:"count"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Video represents a video (minimal)
|
||||
type Video struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Duration int `json:"duration"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
PreviewURI string `json:"preview_uri"`
|
||||
Available bool `json:"available"`
|
||||
Views int `json:"views"`
|
||||
Likes int `json:"likes"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Owner *User `json:"owner"`
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Experiments represents experiments
|
||||
type Experiments struct {
|
||||
BaseModel
|
||||
Experiments []ExperimentDetail `json:"experiments"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes Experiments from JSON
|
||||
func (e *Experiments) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
experiments := &Experiments{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["experiments"]; ok {
|
||||
var items []ExperimentDetail
|
||||
json.Unmarshal(v, &items)
|
||||
experiments.Experiments = items
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &experiments.Total)
|
||||
}
|
||||
|
||||
return experiments, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Experiments
|
||||
func (e *Experiments) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
e := &Experiments{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, e); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, e)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExperimentDetail represents experiment details
|
||||
type ExperimentDetail struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Variant string `json:"variant"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes ExperimentDetail from JSON
|
||||
func (e *ExperimentDetail) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
detail := &ExperimentDetail{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &detail.ID)
|
||||
}
|
||||
if v, ok := raw["name"]; ok {
|
||||
json.Unmarshal(v, &detail.Name)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &detail.Description)
|
||||
}
|
||||
if v, ok := raw["status"]; ok {
|
||||
json.Unmarshal(v, &detail.Status)
|
||||
}
|
||||
if v, ok := raw["variant"]; ok {
|
||||
json.Unmarshal(v, &detail.Variant)
|
||||
}
|
||||
if v, ok := raw["start_date"]; ok {
|
||||
json.Unmarshal(v, &detail.StartDate)
|
||||
}
|
||||
if v, ok := raw["end_date"]; ok {
|
||||
json.Unmarshal(v, &detail.EndDate)
|
||||
}
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of ExperimentDetails
|
||||
func (e *ExperimentDetail) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
detail := &ExperimentDetail{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, detail); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, detail)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Permissions represents user permissions
|
||||
type Permissions struct {
|
||||
BaseModel
|
||||
CanPlay bool `json:"can_play"`
|
||||
CanDownload bool `json:"can_download"`
|
||||
CanLike bool `json:"can_like"`
|
||||
CanCreatePlaylist bool `json:"can_create_playlist"`
|
||||
CanSubscribe bool `json:"can_subscribe"`
|
||||
CanUsePlus bool `json:"can_use_plus"`
|
||||
CanUseRadio bool `json:"can_use_radio"`
|
||||
CanUseYnison bool `json:"can_use_ynison"`
|
||||
CanUseHighQuality bool `json:"can_use_high_quality"`
|
||||
CanUseLossless bool `json:"can_use_lossless"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes Permissions from JSON
|
||||
func (p *Permissions) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
perms := &Permissions{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["can_play"]; ok {
|
||||
json.Unmarshal(v, &perms.CanPlay)
|
||||
}
|
||||
if v, ok := raw["can_download"]; ok {
|
||||
json.Unmarshal(v, &perms.CanDownload)
|
||||
}
|
||||
if v, ok := raw["can_like"]; ok {
|
||||
json.Unmarshal(v, &perms.CanLike)
|
||||
}
|
||||
if v, ok := raw["can_create_playlist"]; ok {
|
||||
json.Unmarshal(v, &perms.CanCreatePlaylist)
|
||||
}
|
||||
if v, ok := raw["can_subscribe"]; ok {
|
||||
json.Unmarshal(v, &perms.CanSubscribe)
|
||||
}
|
||||
if v, ok := raw["can_use_plus"]; ok {
|
||||
json.Unmarshal(v, &perms.CanUsePlus)
|
||||
}
|
||||
if v, ok := raw["can_use_radio"]; ok {
|
||||
json.Unmarshal(v, &perms.CanUseRadio)
|
||||
}
|
||||
if v, ok := raw["can_use_ynison"]; ok {
|
||||
json.Unmarshal(v, &perms.CanUseYnison)
|
||||
}
|
||||
if v, ok := raw["can_use_high_quality"]; ok {
|
||||
json.Unmarshal(v, &perms.CanUseHighQuality)
|
||||
}
|
||||
if v, ok := raw["can_use_lossless"]; ok {
|
||||
json.Unmarshal(v, &perms.CanUseLossless)
|
||||
}
|
||||
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Permissions
|
||||
func (p *Permissions) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
perms := &Permissions{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, perms); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, perms)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PermissionAlerts represents permission alerts
|
||||
type PermissionAlerts struct {
|
||||
BaseModel
|
||||
Alerts []PermissionAlert `json:"alerts"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes PermissionAlerts from JSON
|
||||
func (p *PermissionAlerts) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts := &PermissionAlerts{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["alerts"]; ok {
|
||||
var items []PermissionAlert
|
||||
json.Unmarshal(v, &items)
|
||||
alerts.Alerts = items
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &alerts.Total)
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of PermissionAlerts
|
||||
func (p *PermissionAlerts) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
alerts := &PermissionAlerts{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, alerts); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, alerts)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PermissionAlert represents a permission alert
|
||||
type PermissionAlert struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Action string `json:"action"`
|
||||
IsRead bool `json:"is_read"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes PermissionAlert from JSON
|
||||
func (p *PermissionAlert) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alert := &PermissionAlert{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &alert.ID)
|
||||
}
|
||||
if v, ok := raw["type"]; ok {
|
||||
json.Unmarshal(v, &alert.Type)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &alert.Title)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &alert.Description)
|
||||
}
|
||||
if v, ok := raw["action"]; ok {
|
||||
json.Unmarshal(v, &alert.Action)
|
||||
}
|
||||
if v, ok := raw["is_read"]; ok {
|
||||
json.Unmarshal(v, &alert.IsRead)
|
||||
}
|
||||
if v, ok := raw["created_at"]; ok {
|
||||
json.Unmarshal(v, &alert.CreatedAt)
|
||||
}
|
||||
|
||||
return alert, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of PermissionAlerts
|
||||
func (p *PermissionAlert) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
alert := &PermissionAlert{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, alert); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, alert)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ContentRestrictions represents content restrictions
|
||||
type ContentRestrictions struct {
|
||||
BaseModel
|
||||
HasExplicitContent bool `json:"has_explicit_content"`
|
||||
RestrictedRegions []string `json:"restricted_regions"`
|
||||
AgeRating int `json:"age_rating"`
|
||||
BlockedTracks []string `json:"blocked_tracks"`
|
||||
BlockedAlbums []string `json:"blocked_albums"`
|
||||
BlockedArtists []string `json:"blocked_artists"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes ContentRestrictions from JSON
|
||||
func (c *ContentRestrictions) DeJSON(data []byte, client ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
restrictions := &ContentRestrictions{BaseModel: *NewBaseModel(client)}
|
||||
|
||||
if v, ok := raw["has_explicit_content"]; ok {
|
||||
json.Unmarshal(v, &restrictions.HasExplicitContent)
|
||||
}
|
||||
if v, ok := raw["restricted_regions"]; ok {
|
||||
json.Unmarshal(v, &restrictions.RestrictedRegions)
|
||||
}
|
||||
if v, ok := raw["age_rating"]; ok {
|
||||
json.Unmarshal(v, &restrictions.AgeRating)
|
||||
}
|
||||
if v, ok := raw["blocked_tracks"]; ok {
|
||||
json.Unmarshal(v, &restrictions.BlockedTracks)
|
||||
}
|
||||
if v, ok := raw["blocked_albums"]; ok {
|
||||
json.Unmarshal(v, &restrictions.BlockedAlbums)
|
||||
}
|
||||
if v, ok := raw["blocked_artists"]; ok {
|
||||
json.Unmarshal(v, &restrictions.BlockedArtists)
|
||||
}
|
||||
|
||||
return restrictions, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of ContentRestrictions
|
||||
func (c *ContentRestrictions) DeList(data []byte, client ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
restrictions := &ContentRestrictions{BaseModel: *NewBaseModel(client)}
|
||||
if err := json.Unmarshal(item, restrictions); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, restrictions)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// LyricsMajor represents a lyrics provider (e.g., Genius, Musixmatch)
|
||||
type LyricsMajor struct {
|
||||
BaseModel
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PrettyName string `json:"pretty_name"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes LyricsMajor from JSON
|
||||
func (lm *LyricsMajor) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
major := &LyricsMajor{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &major.ID)
|
||||
}
|
||||
if v, ok := raw["name"]; ok {
|
||||
json.Unmarshal(v, &major.Name)
|
||||
}
|
||||
if v, ok := raw["pretty_name"]; ok {
|
||||
json.Unmarshal(v, &major.PrettyName)
|
||||
}
|
||||
|
||||
return major, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of LyricsMajor
|
||||
func (lm *LyricsMajor) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
major := &LyricsMajor{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, major); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, major)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TrackLyrics represents lyrics information from /tracks/{id}/lyrics endpoint
|
||||
type TrackLyrics struct {
|
||||
BaseModel
|
||||
DownloadURL string `json:"download_url"`
|
||||
LyricID int `json:"lyric_id"`
|
||||
ExternalLyricID string `json:"external_lyric_id"`
|
||||
Writers []string `json:"writers"`
|
||||
Major *LyricsMajor `json:"major"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes TrackLyrics from JSON
|
||||
func (tl *TrackLyrics) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lyrics := &TrackLyrics{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["download_url"]; ok {
|
||||
json.Unmarshal(v, &lyrics.DownloadURL)
|
||||
}
|
||||
if v, ok := raw["lyric_id"]; ok {
|
||||
json.Unmarshal(v, &lyrics.LyricID)
|
||||
}
|
||||
if v, ok := raw["external_lyric_id"]; ok {
|
||||
json.Unmarshal(v, &lyrics.ExternalLyricID)
|
||||
}
|
||||
if v, ok := raw["writers"]; ok {
|
||||
json.Unmarshal(v, &lyrics.Writers)
|
||||
}
|
||||
if v, ok := raw["major"]; ok {
|
||||
var major LyricsMajor
|
||||
json.Unmarshal(v, &major)
|
||||
lyrics.Major = &major
|
||||
}
|
||||
|
||||
return lyrics, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of TrackLyrics
|
||||
func (tl *TrackLyrics) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
lyrics := &TrackLyrics{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, lyrics); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, lyrics)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SupplementLyrics represents the actual lyrics content (from supplement)
|
||||
type SupplementLyrics struct {
|
||||
BaseModel
|
||||
ID int `json:"id"`
|
||||
Text string `json:"lyrics"` // First lines of lyrics
|
||||
FullLyrics string `json:"full_lyrics"` // Full lyrics text
|
||||
HasRights bool `json:"has_rights"`
|
||||
ShowTranslation bool `json:"show_translation"`
|
||||
TextLanguage string `json:"text_language"`
|
||||
URL string `json:"url"`
|
||||
Major *LyricsMajor `json:"major"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes SupplementLyrics from JSON
|
||||
func (l *SupplementLyrics) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lyrics := &SupplementLyrics{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &lyrics.ID)
|
||||
}
|
||||
if v, ok := raw["lyrics"]; ok {
|
||||
json.Unmarshal(v, &lyrics.Text)
|
||||
}
|
||||
if v, ok := raw["full_lyrics"]; ok {
|
||||
json.Unmarshal(v, &lyrics.FullLyrics)
|
||||
}
|
||||
if v, ok := raw["has_rights"]; ok {
|
||||
json.Unmarshal(v, &lyrics.HasRights)
|
||||
}
|
||||
if v, ok := raw["show_translation"]; ok {
|
||||
json.Unmarshal(v, &lyrics.ShowTranslation)
|
||||
}
|
||||
if v, ok := raw["text_language"]; ok {
|
||||
json.Unmarshal(v, &lyrics.TextLanguage)
|
||||
}
|
||||
if v, ok := raw["url"]; ok {
|
||||
json.Unmarshal(v, &lyrics.URL)
|
||||
}
|
||||
if v, ok := raw["major"]; ok {
|
||||
var major LyricsMajor
|
||||
json.Unmarshal(v, &major)
|
||||
lyrics.Major = &major
|
||||
}
|
||||
|
||||
return lyrics, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of SupplementLyrics
|
||||
func (l *SupplementLyrics) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
lyrics := &SupplementLyrics{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, lyrics); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, lyrics)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LyricsInfo represents lyrics availability information
|
||||
type LyricsInfo struct {
|
||||
BaseModel
|
||||
TrackID string `json:"track_id"`
|
||||
Lyrics *SupplementLyrics `json:"lyrics"`
|
||||
Text string `json:"text"`
|
||||
Language string `json:"language"`
|
||||
HasText bool `json:"has_text"`
|
||||
HasSync bool `json:"has_sync"`
|
||||
SyncType string `json:"sync_type"`
|
||||
Major *LyricsMajor `json:"major"`
|
||||
Available bool `json:"available"`
|
||||
HasAvailableSyncLyrics bool `json:"has_available_sync_lyrics"`
|
||||
HasAvailableTextLyrics bool `json:"has_available_text_lyrics"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes LyricsInfo from JSON
|
||||
func (li *LyricsInfo) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &LyricsInfo{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["track_id"]; ok {
|
||||
json.Unmarshal(v, &info.TrackID)
|
||||
}
|
||||
if v, ok := raw["text"]; ok {
|
||||
json.Unmarshal(v, &info.Text)
|
||||
}
|
||||
if v, ok := raw["language"]; ok {
|
||||
json.Unmarshal(v, &info.Language)
|
||||
}
|
||||
if v, ok := raw["has_text"]; ok {
|
||||
json.Unmarshal(v, &info.HasText)
|
||||
}
|
||||
if v, ok := raw["has_sync"]; ok {
|
||||
json.Unmarshal(v, &info.HasSync)
|
||||
}
|
||||
if v, ok := raw["sync_type"]; ok {
|
||||
json.Unmarshal(v, &info.SyncType)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &info.Available)
|
||||
}
|
||||
if v, ok := raw["has_available_sync_lyrics"]; ok {
|
||||
json.Unmarshal(v, &info.HasAvailableSyncLyrics)
|
||||
}
|
||||
if v, ok := raw["has_available_text_lyrics"]; ok {
|
||||
json.Unmarshal(v, &info.HasAvailableTextLyrics)
|
||||
}
|
||||
|
||||
if v, ok := raw["lyrics"]; ok {
|
||||
var lyrics SupplementLyrics
|
||||
json.Unmarshal(v, &lyrics)
|
||||
info.Lyrics = &lyrics
|
||||
}
|
||||
if v, ok := raw["major"]; ok {
|
||||
var major LyricsMajor
|
||||
json.Unmarshal(v, &major)
|
||||
info.Major = &major
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of LyricsInfo
|
||||
func (li *LyricsInfo) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
info := &LyricsInfo{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, info); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, info)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// VideoSupplement represents a video supplement (from supplement endpoint)
|
||||
type VideoSupplement struct {
|
||||
BaseModel
|
||||
Cover string `json:"cover"`
|
||||
Provider string `json:"provider"`
|
||||
Title string `json:"title"`
|
||||
ProviderVideoID string `json:"provider_video_id"`
|
||||
URL string `json:"url"`
|
||||
EmbedURL string `json:"embed_url"`
|
||||
Embed string `json:"embed"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes VideoSupplement from JSON
|
||||
func (vs *VideoSupplement) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supp := &VideoSupplement{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["cover"]; ok {
|
||||
json.Unmarshal(v, &supp.Cover)
|
||||
}
|
||||
if v, ok := raw["provider"]; ok {
|
||||
json.Unmarshal(v, &supp.Provider)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &supp.Title)
|
||||
}
|
||||
if v, ok := raw["provider_video_id"]; ok {
|
||||
json.Unmarshal(v, &supp.ProviderVideoID)
|
||||
}
|
||||
if v, ok := raw["url"]; ok {
|
||||
json.Unmarshal(v, &supp.URL)
|
||||
}
|
||||
if v, ok := raw["embed_url"]; ok {
|
||||
json.Unmarshal(v, &supp.EmbedURL)
|
||||
}
|
||||
if v, ok := raw["embed"]; ok {
|
||||
json.Unmarshal(v, &supp.Embed)
|
||||
}
|
||||
|
||||
return supp, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of VideoSupplements
|
||||
func (vs *VideoSupplement) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
supp := &VideoSupplement{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, supp); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, supp)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Supplement represents a track supplement
|
||||
type Supplement struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Lyrics *LyricsInfo `json:"lyrics"`
|
||||
Videos []VideoSupplement `json:"videos"`
|
||||
RadioIsAvailable bool `json:"radio_is_available"`
|
||||
Description string `json:"description"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes Supplement from JSON
|
||||
func (s *Supplement) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supp := &Supplement{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &supp.ID)
|
||||
}
|
||||
if v, ok := raw["radio_is_available"]; ok {
|
||||
json.Unmarshal(v, &supp.RadioIsAvailable)
|
||||
}
|
||||
if v, ok := raw["description"]; ok {
|
||||
json.Unmarshal(v, &supp.Description)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &supp.Available)
|
||||
}
|
||||
|
||||
if v, ok := raw["lyrics"]; ok {
|
||||
var lyrics LyricsInfo
|
||||
json.Unmarshal(v, &lyrics)
|
||||
supp.Lyrics = &lyrics
|
||||
}
|
||||
if v, ok := raw["videos"]; ok {
|
||||
var videos []VideoSupplement
|
||||
json.Unmarshal(v, &videos)
|
||||
supp.Videos = videos
|
||||
}
|
||||
|
||||
return supp, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Supplements
|
||||
func (s *Supplement) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
supp := &Supplement{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, supp); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, supp)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Track represents a full track
|
||||
type Track struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DurationMs int `json:"duration_ms"`
|
||||
StorageID string `json:"storage_id"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Artists []Artist `json:"artists"`
|
||||
Albums []Album `json:"albums"`
|
||||
Genre string `json:"genre"`
|
||||
Version string `json:"version"`
|
||||
Explicit bool `json:"explicit"`
|
||||
Available bool `json:"available"`
|
||||
AvailableForPremiumUsers bool `json:"available_for_premium_users"`
|
||||
R128 float64 `json:"r128"`
|
||||
TrackSharingFlag string `json:"track_sharing_flag"`
|
||||
Major *Major `json:"major"`
|
||||
Lyrics *TrackLyricsInfo `json:"lyrics"`
|
||||
SimilarTracks []Track `json:"similar_tracks"`
|
||||
DownloadInfo []DownloadInfo `json:"download_info"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Track from JSON
|
||||
func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
track := &Track{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
// Simple fields
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &track.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &track.Title)
|
||||
}
|
||||
if v, ok := raw["duration_ms"]; ok {
|
||||
json.Unmarshal(v, &track.DurationMs)
|
||||
}
|
||||
if v, ok := raw["storage_id"]; ok {
|
||||
json.Unmarshal(v, &track.StorageID)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &track.CoverURI)
|
||||
}
|
||||
if v, ok := raw["genre"]; ok {
|
||||
json.Unmarshal(v, &track.Genre)
|
||||
}
|
||||
if v, ok := raw["version"]; ok {
|
||||
json.Unmarshal(v, &track.Version)
|
||||
}
|
||||
if v, ok := raw["explicit"]; ok {
|
||||
json.Unmarshal(v, &track.Explicit)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &track.Available)
|
||||
}
|
||||
if v, ok := raw["available_for_premium_users"]; ok {
|
||||
json.Unmarshal(v, &track.AvailableForPremiumUsers)
|
||||
}
|
||||
if v, ok := raw["r128"]; ok {
|
||||
json.Unmarshal(v, &track.R128)
|
||||
}
|
||||
if v, ok := raw["track_sharing_flag"]; ok {
|
||||
json.Unmarshal(v, &track.TrackSharingFlag)
|
||||
}
|
||||
|
||||
// Nested objects
|
||||
if v, ok := raw["artists"]; ok {
|
||||
var artists []Artist
|
||||
if err := json.Unmarshal(v, &artists); err == nil {
|
||||
track.Artists = artists
|
||||
}
|
||||
}
|
||||
if v, ok := raw["albums"]; ok {
|
||||
var albums []Album
|
||||
if err := json.Unmarshal(v, &albums); err == nil {
|
||||
track.Albums = albums
|
||||
}
|
||||
}
|
||||
if v, ok := raw["major"]; ok {
|
||||
var major Major
|
||||
if err := json.Unmarshal(v, &major); err == nil {
|
||||
track.Major = &major
|
||||
}
|
||||
}
|
||||
if v, ok := raw["lyrics"]; ok {
|
||||
var lyrics TrackLyricsInfo
|
||||
if err := json.Unmarshal(v, &lyrics); err == nil {
|
||||
track.Lyrics = &lyrics
|
||||
}
|
||||
}
|
||||
if v, ok := raw["similar_tracks"]; ok {
|
||||
var similar []Track
|
||||
if err := json.Unmarshal(v, &similar); err == nil {
|
||||
track.SimilarTracks = similar
|
||||
}
|
||||
}
|
||||
if v, ok := raw["download_info"]; ok {
|
||||
var downloadInfo []DownloadInfo
|
||||
if err := json.Unmarshal(v, &downloadInfo); err == nil {
|
||||
track.DownloadInfo = downloadInfo
|
||||
}
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Tracks
|
||||
func (t *Track) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
track := &Track{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, track); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, track)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetDownloadInfo returns download info for a specific quality
|
||||
func (t *Track) GetDownloadInfo(quality string) *DownloadInfo {
|
||||
for _, info := range t.DownloadInfo {
|
||||
if info.Quality == quality {
|
||||
return &info
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: Artist and Album types are defined in their respective files
|
||||
// These are imported automatically since they're in the same package
|
||||
|
||||
// Major represents major info
|
||||
type Major struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// TrackLyricsInfo represents minimal lyrics info for a track
|
||||
type TrackLyricsInfo struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Major *Major `json:"major"`
|
||||
Available bool `json:"available"`
|
||||
HasText bool `json:"has_text"`
|
||||
}
|
||||
|
||||
// DownloadInfo represents file download info
|
||||
type DownloadInfo struct {
|
||||
Quality string `json:"quality"`
|
||||
Codec string `json:"codec"`
|
||||
Bitrate int `json:"bitrate"`
|
||||
Size int64 `json:"size"`
|
||||
URL string `json:"url"`
|
||||
DirectLink bool `json:"direct_link"`
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// TrackShort represents a lightweight track (used in lists)
|
||||
type TrackShort struct {
|
||||
BaseModel
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DurationMs int `json:"duration_ms"`
|
||||
StorageID string `json:"storage_id"`
|
||||
CoverURI string `json:"cover_uri"`
|
||||
Artists []Artist `json:"artists"`
|
||||
Albums []Album `json:"albums"`
|
||||
Genre string `json:"genre"`
|
||||
Version string `json:"version"`
|
||||
Explicit bool `json:"explicit"`
|
||||
Available bool `json:"available"`
|
||||
AvailableForPremiumUsers bool `json:"available_for_premium_users"`
|
||||
R128 float64 `json:"r128"`
|
||||
TrackSharingFlag string `json:"track_sharing_flag"`
|
||||
Major *Major `json:"major"`
|
||||
Lyrics *SupplementLyrics `json:"lyrics"`
|
||||
SimilarTracks []TrackShort `json:"similar_tracks"`
|
||||
DownloadInfo []DownloadInfo `json:"download_info"`
|
||||
// Additional fields from track_short.py
|
||||
Position *TrackPosition `json:"position"`
|
||||
AvailableAsRingtone bool `json:"available_as_ringtone"`
|
||||
RingtoneID string `json:"ringtone_id"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a TrackShort from JSON
|
||||
func (t *TrackShort) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
track := &TrackShort{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
// Simple fields
|
||||
if v, ok := raw["id"]; ok {
|
||||
json.Unmarshal(v, &track.ID)
|
||||
}
|
||||
if v, ok := raw["title"]; ok {
|
||||
json.Unmarshal(v, &track.Title)
|
||||
}
|
||||
if v, ok := raw["duration_ms"]; ok {
|
||||
json.Unmarshal(v, &track.DurationMs)
|
||||
}
|
||||
if v, ok := raw["storage_id"]; ok {
|
||||
json.Unmarshal(v, &track.StorageID)
|
||||
}
|
||||
if v, ok := raw["cover_uri"]; ok {
|
||||
json.Unmarshal(v, &track.CoverURI)
|
||||
}
|
||||
if v, ok := raw["genre"]; ok {
|
||||
json.Unmarshal(v, &track.Genre)
|
||||
}
|
||||
if v, ok := raw["version"]; ok {
|
||||
json.Unmarshal(v, &track.Version)
|
||||
}
|
||||
if v, ok := raw["explicit"]; ok {
|
||||
json.Unmarshal(v, &track.Explicit)
|
||||
}
|
||||
if v, ok := raw["available"]; ok {
|
||||
json.Unmarshal(v, &track.Available)
|
||||
}
|
||||
if v, ok := raw["available_for_premium_users"]; ok {
|
||||
json.Unmarshal(v, &track.AvailableForPremiumUsers)
|
||||
}
|
||||
if v, ok := raw["r128"]; ok {
|
||||
json.Unmarshal(v, &track.R128)
|
||||
}
|
||||
if v, ok := raw["track_sharing_flag"]; ok {
|
||||
json.Unmarshal(v, &track.TrackSharingFlag)
|
||||
}
|
||||
if v, ok := raw["available_as_ringtone"]; ok {
|
||||
json.Unmarshal(v, &track.AvailableAsRingtone)
|
||||
}
|
||||
if v, ok := raw["ringtone_id"]; ok {
|
||||
json.Unmarshal(v, &track.RingtoneID)
|
||||
}
|
||||
|
||||
// Nested objects
|
||||
if v, ok := raw["artists"]; ok {
|
||||
var artists []Artist
|
||||
if err := json.Unmarshal(v, &artists); err == nil {
|
||||
track.Artists = artists
|
||||
}
|
||||
}
|
||||
if v, ok := raw["albums"]; ok {
|
||||
var albums []Album
|
||||
if err := json.Unmarshal(v, &albums); err == nil {
|
||||
track.Albums = albums
|
||||
}
|
||||
}
|
||||
if v, ok := raw["major"]; ok {
|
||||
var major Major
|
||||
if err := json.Unmarshal(v, &major); err == nil {
|
||||
track.Major = &major
|
||||
}
|
||||
}
|
||||
if v, ok := raw["lyrics"]; ok {
|
||||
var lyrics SupplementLyrics
|
||||
if err := json.Unmarshal(v, &lyrics); err == nil {
|
||||
track.Lyrics = &lyrics
|
||||
}
|
||||
}
|
||||
if v, ok := raw["similar_tracks"]; ok {
|
||||
var similar []TrackShort
|
||||
if err := json.Unmarshal(v, &similar); err == nil {
|
||||
track.SimilarTracks = similar
|
||||
}
|
||||
}
|
||||
if v, ok := raw["download_info"]; ok {
|
||||
var downloadInfo []DownloadInfo
|
||||
if err := json.Unmarshal(v, &downloadInfo); err == nil {
|
||||
track.DownloadInfo = downloadInfo
|
||||
}
|
||||
}
|
||||
if v, ok := raw["position"]; ok {
|
||||
var pos TrackPosition
|
||||
if err := json.Unmarshal(v, &pos); err == nil {
|
||||
track.Position = &pos
|
||||
}
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of TrackShort
|
||||
func (t *TrackShort) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
track := &TrackShort{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, track); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, track)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TracksList represents a list of tracks with metadata
|
||||
type TracksList struct {
|
||||
BaseModel
|
||||
Tracks []TrackShort `json:"tracks"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Pager *Pager `json:"pager"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a TracksList from JSON
|
||||
func (t *TracksList) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := &TracksList{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["tracks"]; ok {
|
||||
var tracks []TrackShort
|
||||
if err := json.Unmarshal(v, &tracks); err == nil {
|
||||
list.Tracks = tracks
|
||||
}
|
||||
}
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &list.Total)
|
||||
}
|
||||
if v, ok := raw["page"]; ok {
|
||||
json.Unmarshal(v, &list.Page)
|
||||
}
|
||||
if v, ok := raw["page_size"]; ok {
|
||||
json.Unmarshal(v, &list.PageSize)
|
||||
}
|
||||
if v, ok := raw["pager"]; ok {
|
||||
var pager Pager
|
||||
if err := json.Unmarshal(v, &pager); err == nil {
|
||||
list.Pager = &pager
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of TracksList
|
||||
func (t *TracksList) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
list := &TracksList{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, list); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, list)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Pager represents pagination info
|
||||
type Pager struct {
|
||||
BaseModel
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Pages int `json:"pages"`
|
||||
PerPage int `json:"per_page"`
|
||||
PrevPage *int `json:"prev_page"`
|
||||
NextPage *int `json:"next_page"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
HasNext bool `json:"has_next"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a Pager from JSON
|
||||
func (p *Pager) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pager := &Pager{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["total"]; ok {
|
||||
json.Unmarshal(v, &pager.Total)
|
||||
}
|
||||
if v, ok := raw["page"]; ok {
|
||||
json.Unmarshal(v, &pager.Page)
|
||||
}
|
||||
if v, ok := raw["page_size"]; ok {
|
||||
json.Unmarshal(v, &pager.PageSize)
|
||||
}
|
||||
if v, ok := raw["pages"]; ok {
|
||||
json.Unmarshal(v, &pager.Pages)
|
||||
}
|
||||
if v, ok := raw["per_page"]; ok {
|
||||
json.Unmarshal(v, &pager.PerPage)
|
||||
}
|
||||
if v, ok := raw["prev_page"]; ok {
|
||||
var prev int
|
||||
if err := json.Unmarshal(v, &prev); err == nil {
|
||||
pager.PrevPage = &prev
|
||||
}
|
||||
}
|
||||
if v, ok := raw["next_page"]; ok {
|
||||
var next int
|
||||
if err := json.Unmarshal(v, &next); err == nil {
|
||||
pager.NextPage = &next
|
||||
}
|
||||
}
|
||||
if v, ok := raw["has_prev"]; ok {
|
||||
json.Unmarshal(v, &pager.HasPrev)
|
||||
}
|
||||
if v, ok := raw["has_next"]; ok {
|
||||
json.Unmarshal(v, &pager.HasNext)
|
||||
}
|
||||
|
||||
return pager, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of Pager
|
||||
func (p *Pager) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
pager := &Pager{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, pager); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, pager)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TrackPosition represents track position in a playlist/queue
|
||||
type TrackPosition struct {
|
||||
BaseModel
|
||||
Revision int `json:"revision"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Position int `json:"position"`
|
||||
FromCache bool `json:"from_cache"`
|
||||
}
|
||||
|
||||
// DeJSON deserializes a TrackPosition from JSON
|
||||
func (tp *TrackPosition) DeJSON(data []byte, c ClientInterface) (Model, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pos := &TrackPosition{BaseModel: *NewBaseModel(c)}
|
||||
|
||||
if v, ok := raw["revision"]; ok {
|
||||
json.Unmarshal(v, &pos.Revision)
|
||||
}
|
||||
if v, ok := raw["timestamp"]; ok {
|
||||
json.Unmarshal(v, &pos.Timestamp)
|
||||
}
|
||||
if v, ok := raw["position"]; ok {
|
||||
json.Unmarshal(v, &pos.Position)
|
||||
}
|
||||
if v, ok := raw["from_cache"]; ok {
|
||||
json.Unmarshal(v, &pos.FromCache)
|
||||
}
|
||||
|
||||
return pos, nil
|
||||
}
|
||||
|
||||
// DeList deserializes a list of TrackPosition
|
||||
func (tp *TrackPosition) DeList(data []byte, c ClientInterface) ([]Model, error) {
|
||||
var rawList []json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]Model, 0, len(rawList))
|
||||
for _, item := range rawList {
|
||||
pos := &TrackPosition{BaseModel: *NewBaseModel(c)}
|
||||
if err := json.Unmarshal(item, pos); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, pos)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mockRequest implements RequestInterface for testing
|
||||
type mockRequest struct{}
|
||||
|
||||
func (m *mockRequest) Get(ctx context.Context, path string, params map[string]string, result any) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockRequest) Post(ctx context.Context, path string, body any, result any) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockRequest) Put(ctx context.Context, path string, body any, result any) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockRequest) Delete(ctx context.Context, path string, body any, result any) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockRequest) Download(ctx context.Context, url string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// mockClient implements ClientInterface for testing
|
||||
type mockClient struct {
|
||||
request *mockRequest
|
||||
}
|
||||
|
||||
func (m *mockClient) GetRequest() RequestInterface {
|
||||
return m.request
|
||||
}
|
||||
|
||||
func newMockClient() *mockClient {
|
||||
return &mockClient{request: &mockRequest{}}
|
||||
}
|
||||
|
||||
func TestTrack_DeJSON(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`{
|
||||
"id": "123",
|
||||
"title": "Test Track",
|
||||
"duration_ms": 180000,
|
||||
"storage_id": "456",
|
||||
"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}
|
||||
}`)
|
||||
|
||||
track := &Track{BaseModel: *NewBaseModel(client)}
|
||||
result, err := track.DeJSON(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeJSON failed: %v", err)
|
||||
}
|
||||
|
||||
resultTrack := result.(*Track)
|
||||
if resultTrack.ID != "123" {
|
||||
t.Errorf("Expected ID '123', got '%s'", resultTrack.ID)
|
||||
}
|
||||
if resultTrack.Title != "Test Track" {
|
||||
t.Errorf("Expected Title 'Test Track', got '%s'", resultTrack.Title)
|
||||
}
|
||||
if resultTrack.DurationMs != 180000 {
|
||||
t.Errorf("Expected DurationMs 180000, got %d", resultTrack.DurationMs)
|
||||
}
|
||||
if len(resultTrack.Artists) != 1 {
|
||||
t.Errorf("Expected 1 artist, got %d", len(resultTrack.Artists))
|
||||
}
|
||||
if resultTrack.Major == nil || resultTrack.Major.Name != "Major Label" {
|
||||
t.Errorf("Expected Major 'Major Label'")
|
||||
}
|
||||
if resultTrack.Lyrics == nil || resultTrack.Lyrics.Text != "Test lyrics" {
|
||||
t.Errorf("Expected Lyrics 'Test lyrics'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrack_DeList(t *testing.T) {
|
||||
client := newMockClient()
|
||||
data := []byte(`[
|
||||
{"id": "1", "title": "Track 1", "duration_ms": 100000},
|
||||
{"id": "2", "title": "Track 2", "duration_ms": 200000}
|
||||
]`)
|
||||
|
||||
track := &Track{BaseModel: *NewBaseModel(client)}
|
||||
result, err := track.DeList(data, client)
|
||||
if err != nil {
|
||||
t.Fatalf("DeList failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 tracks, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrack_GetDownloadInfo(t *testing.T) {
|
||||
client := newMockClient()
|
||||
track := &Track{
|
||||
BaseModel: *NewBaseModel(client),
|
||||
DownloadInfo: []DownloadInfo{
|
||||
{Quality: "mp3", Codec: "mp3", Bitrate: 320, Size: 5000000, URL: "https://example.com/track.mp3"},
|
||||
{Quality: "flac", Codec: "flac", Bitrate: 1411, Size: 25000000, URL: "https://example.com/track.flac"},
|
||||
},
|
||||
}
|
||||
|
||||
info := track.GetDownloadInfo("mp3")
|
||||
if info == nil {
|
||||
t.Fatal("Expected mp3 download info")
|
||||
}
|
||||
if info.Bitrate != 320 {
|
||||
t.Errorf("Expected bitrate 320, got %d", info.Bitrate)
|
||||
}
|
||||
|
||||
info = track.GetDownloadInfo("flac")
|
||||
if info == nil {
|
||||
t.Fatal("Expected flac download info")
|
||||
}
|
||||
if info.Bitrate != 1411 {
|
||||
t.Errorf("Expected bitrate 1411, got %d", info.Bitrate)
|
||||
}
|
||||
|
||||
info = track.GetDownloadInfo("ogg")
|
||||
if info != nil {
|
||||
t.Errorf("Expected nil for ogg, got %v", info)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JSONBackend defines the interface for JSON encoding/decoding
|
||||
type JSONBackend interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
|
||||
// DefaultJSONBackend uses the standard library encoding/json
|
||||
type DefaultJSONBackend struct{}
|
||||
|
||||
func (DefaultJSONBackend) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (DefaultJSONBackend) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// GetDefaultJSONBackend returns the default JSON backend
|
||||
func GetDefaultJSONBackend() JSONBackend {
|
||||
return DefaultJSONBackend{}
|
||||
}
|
||||
|
||||
// RawMessage is a raw encoded JSON value
|
||||
type RawMessage = json.RawMessage
|
||||
|
||||
// Marshal is a convenience function
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
return GetDefaultJSONBackend().Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal is a convenience function
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
return GetDefaultJSONBackend().Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// ToSnakeCase converts camelCase to snake_case
|
||||
func ToSnakeCase(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
var result strings.Builder
|
||||
for i, r := range s {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
if i > 0 {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
result.WriteRune(r + 'a' - 'A')
|
||||
} else {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ToCamelCase converts snake_case to camelCase
|
||||
func ToCamelCase(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
parts := strings.Split(s, "_")
|
||||
if len(parts) == 1 {
|
||||
return s
|
||||
}
|
||||
var result strings.Builder
|
||||
result.WriteString(parts[0])
|
||||
for _, part := range parts[1:] {
|
||||
if len(part) > 0 {
|
||||
result.WriteString(strings.ToUpper(part[:1]) + part[1:])
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// MapKeysToSnakeCase converts all keys in a map to snake_case
|
||||
func MapKeysToSnakeCase(m map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
result[ToSnakeCase(k)] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeysToCamelCase converts all keys in a map to camelCase
|
||||
func MapKeysToCamelCase(m map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
result[ToCamelCase(k)] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// StructToMap converts a struct to a map using json tags
|
||||
func StructToMap(v any, forRequest bool) (map[string]any, error) {
|
||||
data, err := Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]any
|
||||
if err := Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetJSONTag returns the json tag for a struct field
|
||||
func GetJSONTag(t reflect.Type, fieldIndex int) string {
|
||||
if fieldIndex >= t.NumField() {
|
||||
return ""
|
||||
}
|
||||
field := t.Field(fieldIndex)
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "" || tag == "-" {
|
||||
return ""
|
||||
}
|
||||
// Parse tag options (e.g., "name,omitempty")
|
||||
parts := strings.Split(tag, ",")
|
||||
return parts[0]
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Response represents an API response
|
||||
type Response struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorDescription string `json:"errorDescription,omitempty"`
|
||||
}
|
||||
|
||||
// GetResult returns the result as raw JSON
|
||||
func (r *Response) GetResult() json.RawMessage {
|
||||
return r.Result
|
||||
}
|
||||
|
||||
// GetError returns the error message
|
||||
func (r *Response) GetError() string {
|
||||
if r.Error != "" {
|
||||
return r.Error
|
||||
}
|
||||
if r.ErrorDescription != "" {
|
||||
return r.ErrorDescription
|
||||
}
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
// HasError returns true if the response contains an error
|
||||
func (r *Response) HasError() bool {
|
||||
return r.Error != "" || r.ErrorDescription != ""
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshaling
|
||||
func (r *Response) UnmarshalJSON(data []byte) error {
|
||||
// First try to unmarshal as a standard response
|
||||
type Alias Response
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If result is missing, wrap the whole response in result
|
||||
if r.Result == nil {
|
||||
r.Result = data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeJSON creates a Response from JSON data
|
||||
func DeJSON(data []byte) (*Response, error) {
|
||||
var resp Response
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Error types
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrNetwork = errors.New("network error")
|
||||
ErrTimeout = errors.New("timeout")
|
||||
ErrSchemaMismatch = errors.New("schema mismatch")
|
||||
)
|
||||
|
||||
// YandexMusicError is the base error type
|
||||
type YandexMusicError struct {
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *YandexMusicError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Message + ": " + e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *YandexMusicError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewYandexMusicError creates a new YandexMusicError
|
||||
func NewYandexMusicError(message string, err error) *YandexMusicError {
|
||||
return &YandexMusicError{Message: message, Err: err}
|
||||
}
|
||||
|
||||
// IsUnauthorizedError checks if error is unauthorized
|
||||
func IsUnauthorizedError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrUnauthorized)
|
||||
}
|
||||
return errors.Is(err, ErrUnauthorized)
|
||||
}
|
||||
|
||||
// IsBadRequestError checks if error is bad request
|
||||
func IsBadRequestError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrBadRequest)
|
||||
}
|
||||
return errors.Is(err, ErrBadRequest)
|
||||
}
|
||||
|
||||
// IsNotFoundError checks if error is not found
|
||||
func IsNotFoundError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrNotFound)
|
||||
}
|
||||
return errors.Is(err, ErrNotFound)
|
||||
}
|
||||
|
||||
// IsNetworkError checks if error is network error
|
||||
func IsNetworkError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrNetwork)
|
||||
}
|
||||
return errors.Is(err, ErrNetwork)
|
||||
}
|
||||
|
||||
// IsTimeoutError checks if error is timeout
|
||||
func IsTimeoutError(err error) bool {
|
||||
var yme *YandexMusicError
|
||||
if errors.As(err, &yme) {
|
||||
return errors.Is(yme.Err, ErrTimeout)
|
||||
}
|
||||
return errors.Is(err, ErrTimeout)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SchemaMismatch represents a mismatch between expected and actual API response
|
||||
type SchemaMismatch struct {
|
||||
ModelName string
|
||||
MissingFields []string
|
||||
UnknownFields []string
|
||||
UnknownValues map[string]any
|
||||
Endpoint string
|
||||
LibraryVersion string
|
||||
}
|
||||
|
||||
func (sm *SchemaMismatch) Describe() string {
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("Model: %s", sm.ModelName))
|
||||
if len(sm.MissingFields) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Missing fields: %v", sm.MissingFields))
|
||||
}
|
||||
if len(sm.UnknownFields) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Unknown fields: %v", sm.UnknownFields))
|
||||
}
|
||||
if len(sm.UnknownValues) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("Unknown field structures: %v", sm.UnknownValues))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("Endpoint: %s", sm.Endpoint))
|
||||
parts = append(parts, fmt.Sprintf("Library version: %s", sm.LibraryVersion))
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// SchemaMismatchHandler is a callback for handling schema mismatches
|
||||
type SchemaMismatchHandler func(*SchemaMismatch)
|
||||
|
||||
// SchemaMismatchConfig holds configuration for schema mismatch handling
|
||||
type SchemaMismatchConfig struct {
|
||||
ReportUnknownFields bool
|
||||
Strict bool
|
||||
OnSchemaMismatch SchemaMismatchHandler
|
||||
}
|
||||
|
||||
// DefaultSchemaMismatchConfig returns the default configuration
|
||||
func DefaultSchemaMismatchConfig() SchemaMismatchConfig {
|
||||
return SchemaMismatchConfig{
|
||||
ReportUnknownFields: false,
|
||||
Strict: false,
|
||||
OnSchemaMismatch: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportSchemaMismatch reports a schema mismatch according to config
|
||||
func ReportSchemaMismatch(config SchemaMismatchConfig, modelName, endpoint string, missingFields, unknownFields []string, unknownValues map[string]any) error {
|
||||
mismatch := &SchemaMismatch{
|
||||
ModelName: modelName,
|
||||
MissingFields: missingFields,
|
||||
UnknownFields: unknownFields,
|
||||
UnknownValues: unknownValues,
|
||||
Endpoint: endpoint,
|
||||
LibraryVersion: "go-port-0.1.0",
|
||||
}
|
||||
|
||||
if config.OnSchemaMismatch != nil {
|
||||
config.OnSchemaMismatch(mismatch)
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := mismatch.Describe()
|
||||
if config.Strict {
|
||||
return fmt.Errorf("schema mismatch: %s", msg)
|
||||
}
|
||||
|
||||
log.Printf("WARNING: Schema mismatch for %s:\n%s", modelName, msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchemaMismatchTracker tracks reported mismatches to avoid duplicates
|
||||
type SchemaMismatchTracker struct {
|
||||
mu sync.Mutex
|
||||
reported map[string]bool
|
||||
}
|
||||
|
||||
func NewSchemaMismatchTracker() *SchemaMismatchTracker {
|
||||
return &SchemaMismatchTracker{
|
||||
reported: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SchemaMismatchTracker) ShouldReport(modelName string, fields []string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
key := modelName + ":" + strings.Join(fields, ",")
|
||||
if t.reported[key] {
|
||||
return false
|
||||
}
|
||||
t.reported[key] = true
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSignKey is the signing key from the Android app
|
||||
DefaultSignKey = "p93jhgh689SBReK6ghtw62"
|
||||
// DesktopSignKey is the signing key from the desktop app
|
||||
DesktopSignKey = "kzqU4XhfCaY6B6JTHODeq5"
|
||||
// DesktopClient is the X-Yandex-Music-Client header value for desktop
|
||||
DesktopClient = "YandexMusicDesktopAppWindows/5.95.0"
|
||||
)
|
||||
|
||||
// Sign represents a request signature
|
||||
type Sign struct {
|
||||
Timestamp int64
|
||||
Value string
|
||||
}
|
||||
|
||||
// GetSignRequest creates a signature for a track request
|
||||
func GetSignRequest(trackID string, key string) Sign {
|
||||
if key == "" {
|
||||
key = DefaultSignKey
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
message := trackID + strconv.FormatInt(timestamp, 10)
|
||||
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return Sign{
|
||||
Timestamp: timestamp,
|
||||
Value: sign,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFileInfoSign creates a signature for file info requests
|
||||
func GetFileInfoSign(trackID, quality string, codecs []string, transport, key string) Sign {
|
||||
if key == "" {
|
||||
key = DesktopSignKey
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
message := fmt.Sprintf("%d%s%s%s%s", timestamp, trackID, quality, joinCodecs(codecs), transport)
|
||||
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
// Remove last character as in Python
|
||||
if len(sign) > 0 {
|
||||
sign = sign[:len(sign)-1]
|
||||
}
|
||||
|
||||
return Sign{
|
||||
Timestamp: timestamp,
|
||||
Value: sign,
|
||||
}
|
||||
}
|
||||
|
||||
func joinCodecs(codecs []string) string {
|
||||
result := ""
|
||||
for _, c := range codecs {
|
||||
result += c
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestGetSignRequest tests the GetSignRequest function with various inputs
|
||||
func TestGetSignRequest(t *testing.T) {
|
||||
// Test with default key
|
||||
sign := GetSignRequest("12345", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test with custom key
|
||||
sign = GetSignRequest("12345", "custom-key")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test track ID as string (function accepts string)
|
||||
sign = GetSignRequest("12345", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp for string track ID")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value for string track ID")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetFileInfoSign tests the GetFileInfoSign function with various inputs
|
||||
func TestGetFileInfoSign(t *testing.T) {
|
||||
codecs := []string{"mp3", "flac"}
|
||||
sign := GetFileInfoSign("12345", "high", codecs, "http", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test with custom key
|
||||
sign = GetFileInfoSign("12345", "high", codecs, "http", "custom-key")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value")
|
||||
}
|
||||
|
||||
// Test empty codecs
|
||||
sign = GetFileInfoSign("12345", "high", []string{}, "http", "")
|
||||
if sign.Timestamp == 0 {
|
||||
t.Error("Expected non-zero timestamp for empty codecs")
|
||||
}
|
||||
if sign.Value == "" {
|
||||
t.Error("Expected non-empty sign value for empty codecs")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignDeterministic tests that same input produces same output with fixed time
|
||||
func TestSignDeterministic(t *testing.T) {
|
||||
// We can't easily test this without mocking time, but we can verify
|
||||
// that the function doesn't panic
|
||||
for i := 0; i < 10; i++ {
|
||||
sign := GetSignRequest("test-track", "test-key")
|
||||
if sign.Value == "" {
|
||||
t.Errorf("Iteration %d: got empty sign", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSignRequestFixedTimestamp tests GetSignRequest with a fixed timestamp
|
||||
// to verify deterministic output matches Python reference implementation
|
||||
func TestGetSignRequestFixedTimestamp(t *testing.T) {
|
||||
// Use a fixed timestamp for deterministic testing
|
||||
fixedTime := time.Unix(1668687184, 0)
|
||||
trackID := "4784420"
|
||||
key := "SUPER_SECRET_KEY"
|
||||
expectedSign := "vssEEweZhgv2Aud0rdH9maOXUC03ZkZ/hlo6bSRN8Qg="
|
||||
|
||||
// Manually compute expected sign with fixed time
|
||||
message := trackID + strconv.FormatInt(fixedTime.Unix(), 10)
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
computedSign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
if computedSign != expectedSign {
|
||||
t.Errorf("Expected sign %q, got %q", expectedSign, computedSign)
|
||||
}
|
||||
|
||||
// Test that our function produces correct sign when we override time
|
||||
// Note: We can't easily mock time.Now() in Go without refactoring,
|
||||
// so we test the algorithm directly here
|
||||
sign := GetSignRequest(trackID, key)
|
||||
_ = sign // Just verify no panic
|
||||
}
|
||||
|
||||
// TestGetFileInfoSignFixedTimestamp tests GetFileInfoSign with a fixed timestamp
|
||||
func TestGetFileInfoSignFixedTimestamp(t *testing.T) {
|
||||
fixedTime := time.Unix(1668687184, 0)
|
||||
trackID := "4784420:100500"
|
||||
quality := "lossless"
|
||||
codecs := []string{"flac", "aac", "mp3"}
|
||||
transport := "encraw"
|
||||
key := "SUPER_SECRET_KEY"
|
||||
|
||||
// Manually compute expected sign with fixed time (matching Python)
|
||||
message := strconv.FormatInt(fixedTime.Unix(), 10) + trackID + quality + "flacaacmp3" + transport
|
||||
h := hmac.New(sha256.New, []byte(key))
|
||||
h.Write([]byte(message))
|
||||
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
// Remove last character as in Python
|
||||
if len(sign) > 0 {
|
||||
sign = sign[:len(sign)-1]
|
||||
}
|
||||
|
||||
// Test our function with fixed time logic
|
||||
sign2 := GetFileInfoSign(trackID, quality, codecs, transport, key)
|
||||
_ = sign2 // Just verify no panic
|
||||
|
||||
// Verify the algorithm produces expected format
|
||||
if sign == "" {
|
||||
t.Error("Expected non-empty computed sign")
|
||||
}
|
||||
if sign[len(sign)-1] == '=' {
|
||||
t.Error("Sign should not end with '=' (last char removed)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestJoinCodecs tests the joinCodecs helper function
|
||||
func TestJoinCodecs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
codecs []string
|
||||
expected string
|
||||
}{
|
||||
{"empty", []string{}, ""},
|
||||
{"single", []string{"mp3"}, "mp3"},
|
||||
{"multiple", []string{"flac", "aac", "mp3"}, "flacaacmp3"},
|
||||
{"with_special_chars", []string{"opus", "aac+"}, "opusaac+"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := joinCodecs(tc.codecs)
|
||||
if result != tc.expected {
|
||||
t.Errorf("joinCodecs(%v) = %q, want %q", tc.codecs, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignWithDifferentKeys tests that different keys produce different signatures
|
||||
func TestSignWithDifferentKeys(t *testing.T) {
|
||||
trackID := "12345"
|
||||
key1 := "key1"
|
||||
key2 := "key2"
|
||||
|
||||
sign1 := GetSignRequest(trackID, key1)
|
||||
sign2 := GetSignRequest(trackID, key2)
|
||||
|
||||
if sign1.Value == sign2.Value {
|
||||
t.Error("Different keys should produce different signatures")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileInfoSignWithDifferentKeys tests that different keys produce different signatures
|
||||
func TestFileInfoSignWithDifferentKeys(t *testing.T) {
|
||||
codecs := []string{"mp3", "flac"}
|
||||
key1 := "key1"
|
||||
key2 := "key2"
|
||||
|
||||
sign1 := GetFileInfoSign("12345", "high", codecs, "http", key1)
|
||||
sign2 := GetFileInfoSign("12345", "high", codecs, "http", key2)
|
||||
|
||||
if sign1.Value == sign2.Value {
|
||||
t.Error("Different keys should produce different signatures")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileInfoSignRemovesLastChar tests that the last character is removed from base64 output
|
||||
func TestFileInfoSignRemovesLastChar(t *testing.T) {
|
||||
codecs := []string{"mp3"}
|
||||
sign := GetFileInfoSign("12345", "high", codecs, "http", "test-key")
|
||||
|
||||
// The sign should not end with '=' (padding) because we remove the last char
|
||||
// Note: it could end with '=' if the original base64 ended with '==', but we remove one char
|
||||
_ = sign
|
||||
// This is more of a sanity check that the function runs
|
||||
}
|
||||
|
||||
// TestDefaultSignKey tests that default keys are used when empty string provided
|
||||
func TestDefaultSignKey(t *testing.T) {
|
||||
// GetSignRequest should use DefaultSignKey when key is empty
|
||||
sign1 := GetSignRequest("12345", "")
|
||||
sign2 := GetSignRequest("12345", DefaultSignKey)
|
||||
|
||||
// They should produce same result (timestamps will differ, but algorithm same)
|
||||
if sign1.Value == "" || sign2.Value == "" {
|
||||
t.Error("Both should produce non-empty signatures")
|
||||
}
|
||||
|
||||
// GetFileInfoSign should use DesktopSignKey when key is empty
|
||||
sign3 := GetFileInfoSign("12345", "high", []string{"mp3"}, "http", "")
|
||||
sign4 := GetFileInfoSign("12345", "high", []string{"mp3"}, "http", DesktopSignKey)
|
||||
|
||||
if sign3.Value == "" || sign4.Value == "" {
|
||||
t.Error("Both should produce non-empty signatures")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTimestampIsRecent tests that timestamp is close to current time
|
||||
func TestTimestampIsRecent(t *testing.T) {
|
||||
before := time.Now().Unix()
|
||||
sign := GetSignRequest("12345", "test-key")
|
||||
after := time.Now().Unix()
|
||||
|
||||
if sign.Timestamp < before || sign.Timestamp > after {
|
||||
t.Errorf("Timestamp %d not in range [%d, %d]", sign.Timestamp, before, after)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package ynison
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/client"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// YnisonClient represents a Ynison WebSocket client
|
||||
type YnisonClient struct {
|
||||
client *client.Client
|
||||
conn *websocket.Conn
|
||||
deviceID string
|
||||
sessionID int64
|
||||
mu sync.Mutex
|
||||
handlers map[string]func(*proto.PutYnisonStateResponse)
|
||||
ridCounter int64
|
||||
connected bool
|
||||
reconnectCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
wsURL string // For testing: override WebSocket URL
|
||||
closeOnce sync.Once // Ensures doneCh is closed only once
|
||||
}
|
||||
|
||||
// DeviceConfig holds device configuration for Ynison
|
||||
type DeviceConfig struct {
|
||||
DeviceID string
|
||||
Title string
|
||||
DeviceType proto.DeviceType
|
||||
AppName string
|
||||
AppVersion string
|
||||
CanBePlayer bool
|
||||
CanBeRemote bool
|
||||
VolumeGran int
|
||||
}
|
||||
|
||||
// NewYnisonClient creates a new Ynison client
|
||||
func NewYnisonClient(c *client.Client, config DeviceConfig) *YnisonClient {
|
||||
if config.DeviceID == "" {
|
||||
config.DeviceID = fmt.Sprintf("go-ynison-%d", time.Now().UnixNano())
|
||||
}
|
||||
if config.Title == "" {
|
||||
config.Title = "Go Ynison Client"
|
||||
}
|
||||
if config.AppName == "" {
|
||||
config.AppName = "YandexMusicGo"
|
||||
}
|
||||
if config.AppVersion == "" {
|
||||
config.AppVersion = "1.0.0"
|
||||
}
|
||||
if config.VolumeGran == 0 {
|
||||
config.VolumeGran = 100
|
||||
}
|
||||
|
||||
return &YnisonClient{
|
||||
client: c,
|
||||
deviceID: config.DeviceID,
|
||||
handlers: make(map[string]func(*proto.PutYnisonStateResponse)),
|
||||
reconnectCh: make(chan struct{}, 1),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes a WebSocket connection to Ynison
|
||||
func (y *YnisonClient) Connect(ctx context.Context) error {
|
||||
token := y.client.GetToken()
|
||||
if token == "" {
|
||||
return fmt.Errorf("no auth token available")
|
||||
}
|
||||
|
||||
wsURL := y.wsURL
|
||||
if wsURL == "" {
|
||||
wsURL = fmt.Sprintf("wss://ynison.music.yandex.ru/redirector.YnisonRedirector/Connect?device_id=%s&token=%s", y.deviceID, token)
|
||||
}
|
||||
|
||||
dialer := websocket.DefaultDialer
|
||||
conn, _, err := dialer.DialContext(ctx, wsURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Ynison: %w", err)
|
||||
}
|
||||
|
||||
y.conn = conn
|
||||
y.connected = true
|
||||
|
||||
// Start reading messages
|
||||
go y.readLoop(ctx)
|
||||
|
||||
// Send initial UpdateFullState
|
||||
return y.sendUpdateFullState(ctx)
|
||||
}
|
||||
|
||||
// overrideWSURL sets a custom WebSocket URL for testing
|
||||
func (y *YnisonClient) overrideWSURL(url string) {
|
||||
y.wsURL = url
|
||||
}
|
||||
|
||||
// Disconnect closes the WebSocket connection
|
||||
func (y *YnisonClient) Disconnect() error {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
|
||||
if y.conn != nil {
|
||||
y.connected = false
|
||||
y.closeOnce.Do(func() {
|
||||
close(y.doneCh)
|
||||
})
|
||||
err := y.conn.Close()
|
||||
y.conn = nil // Prevent double close
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if the client is connected
|
||||
func (y *YnisonClient) IsConnected() bool {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
return y.connected
|
||||
}
|
||||
|
||||
// readLoop reads messages from the WebSocket connection
|
||||
func (y *YnisonClient) readLoop(ctx context.Context) {
|
||||
defer func() {
|
||||
y.mu.Lock()
|
||||
y.connected = false
|
||||
y.mu.Unlock()
|
||||
y.closeOnce.Do(func() {
|
||||
close(y.doneCh)
|
||||
})
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-y.doneCh:
|
||||
return
|
||||
default:
|
||||
_, message, err := y.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
fmt.Printf("Ynison read error: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var resp proto.PutYnisonStateResponse
|
||||
if err := json.Unmarshal(message, &resp); err != nil {
|
||||
fmt.Printf("Ynison unmarshal error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Call registered handlers
|
||||
y.mu.Lock()
|
||||
for _, handler := range y.handlers {
|
||||
go handler(&resp)
|
||||
}
|
||||
y.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendUpdateFullState sends the initial full state update
|
||||
func (y *YnisonClient) sendUpdateFullState(ctx context.Context) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdateFullState: &proto.UpdateFullState{
|
||||
PlayerState: &proto.PlayerState{
|
||||
Status: &proto.PlayingStatus{
|
||||
ProgressMs: 0,
|
||||
DurationMs: 0,
|
||||
Paused: true,
|
||||
PlaybackSpeed: 1.0,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
PlayerQueue: &proto.PlayerQueue{
|
||||
EntityId: "",
|
||||
EntityType: proto.PlayerQueueEntityType_ENTITY_TYPE_UNSPECIFIED,
|
||||
CurrentPlayableIndex: -1,
|
||||
Options: &proto.PlayerStateOptions{
|
||||
RepeatMode: proto.PlayerStateOptionsRepeatMode_REPEAT_MODE_NONE,
|
||||
},
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
EntityContext: proto.PlayerQueueEntityContext_ENTITY_CONTEXT_UNSPECIFIED,
|
||||
},
|
||||
PlayerQueueInjectOptional: &proto.PlayerQueueInject{
|
||||
PlayingStatus: &proto.PlayingStatus{
|
||||
ProgressMs: 0,
|
||||
DurationMs: 0,
|
||||
Paused: true,
|
||||
PlaybackSpeed: 1.0,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
Playable: &proto.PlayerQueueInjectPlayable{
|
||||
PlayableId: "",
|
||||
PlayableType: proto.PlayerQueueInjectPlayablePlayableType_INJECT_PLAYABLE_TYPE_UNSPECIFIED,
|
||||
Title: "",
|
||||
},
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
},
|
||||
IsCurrentlyActive: false,
|
||||
Device: &proto.UpdateDevice{
|
||||
Info: &proto.DeviceInfo{
|
||||
DeviceId: y.deviceID,
|
||||
Title: "Go Ynison Client",
|
||||
Type: proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP,
|
||||
AppName: "YandexMusicGo",
|
||||
AppVersion: "1.0.0",
|
||||
},
|
||||
Volume: 0.5,
|
||||
Capabilities: &proto.DeviceCapabilities{
|
||||
CanBePlayer: true,
|
||||
CanBeRemoteController: true,
|
||||
VolumeGranularity: 100,
|
||||
},
|
||||
VolumeInfo: &proto.DeviceVolume{
|
||||
Volume: 0.5,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: y.deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
ActivityInterceptionType: proto.PutYnisonStateRequestActivityInterceptionType_INTERCEPTION_DO_NOT_INTERCEPT_BY_DEFAULT,
|
||||
}
|
||||
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// nextRid generates the next request ID
|
||||
func (y *YnisonClient) nextRid() string {
|
||||
y.ridCounter++
|
||||
return fmt.Sprintf("%s-%d", y.deviceID, y.ridCounter)
|
||||
}
|
||||
|
||||
// sendRequest sends a request to the Ynison server
|
||||
func (y *YnisonClient) sendRequest(ctx context.Context, req *proto.PutYnisonStateRequest) error {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
|
||||
if !y.connected || y.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
return y.conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
// RegisterHandler registers a response handler
|
||||
func (y *YnisonClient) RegisterHandler(name string, handler func(*proto.PutYnisonStateResponse)) {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
y.handlers[name] = handler
|
||||
}
|
||||
|
||||
// UnregisterHandler removes a response handler
|
||||
func (y *YnisonClient) UnregisterHandler(name string) {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
delete(y.handlers, name)
|
||||
}
|
||||
|
||||
// UpdatePlayingStatus updates the playing status
|
||||
func (y *YnisonClient) UpdatePlayingStatus(ctx context.Context, status *proto.PlayingStatus) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdatePlayingStatus: &proto.UpdatePlayingStatus{
|
||||
PlayingStatus: status,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateVolume updates the volume
|
||||
func (y *YnisonClient) UpdateVolume(ctx context.Context, deviceID string, volume float64, version *proto.UpdateVersion) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdateVolumeInfo: &proto.UpdateVolumeInfo{
|
||||
DeviceId: deviceID,
|
||||
VolumeInfo: &proto.DeviceVolume{
|
||||
Volume: volume,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdateActiveDevice updates the active device
|
||||
func (y *YnisonClient) UpdateActiveDevice(ctx context.Context, deviceID string) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdateActiveDevice: &proto.UpdateActiveDevice{
|
||||
DeviceIdOptional: deviceID,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdatePlayerState updates the player state (queue, repeat, shuffle)
|
||||
func (y *YnisonClient) UpdatePlayerState(ctx context.Context, state *proto.PlayerState) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdatePlayerState: &proto.UpdatePlayerState{
|
||||
PlayerState: state,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// UpdatePlayerQueueInject updates the injected queue entity
|
||||
func (y *YnisonClient) UpdatePlayerQueueInject(ctx context.Context, inject *proto.PlayerQueueInject) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
UpdatePlayerQueueInject: &proto.UpdatePlayerQueueInject{
|
||||
PlayerQueueInject: inject,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// SyncStateFromEOV requests synchronization from EOV
|
||||
func (y *YnisonClient) SyncStateFromEOV(ctx context.Context, actualQueueID string) error {
|
||||
req := &proto.PutYnisonStateRequest{
|
||||
SyncStateFromEov: &proto.SyncStateFromEOV{
|
||||
ActualQueueId: actualQueueID,
|
||||
},
|
||||
PlayerActionTimestampMs: time.Now().UnixMilli(),
|
||||
Rid: y.nextRid(),
|
||||
}
|
||||
return y.sendRequest(ctx, req)
|
||||
}
|
||||
|
||||
// GetDevices returns the list of connected devices from the last response
|
||||
func (y *YnisonClient) GetDevices() []*proto.Device {
|
||||
// This would need to be implemented with proper state tracking
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveDeviceID returns the active device ID from the last response
|
||||
func (y *YnisonClient) GetActiveDeviceID() string {
|
||||
// This would need to be implemented with proper state tracking
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPlayerState returns the current player state from the last response
|
||||
func (y *YnisonClient) GetPlayerState() *proto.PlayerState {
|
||||
// This would need to be implemented with proper state tracking
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitForConnection waits for the connection to be established or context to be done
|
||||
func (y *YnisonClient) WaitForConnection(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-y.doneCh:
|
||||
return fmt.Errorf("connection closed")
|
||||
default:
|
||||
if y.IsConnected() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package ynison
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/client"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// TestYnisonClient_Connect tests the Connect method with a mock WebSocket server
|
||||
func TestYnisonClient_Connect(t *testing.T) {
|
||||
// Create a mock WebSocket server
|
||||
var receivedMessages [][]byte
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
t.Logf("Upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read messages from client
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
t.Logf("Read error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
receivedMessages = append(receivedMessages, message)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Convert HTTP URL to WS URL
|
||||
wsURL := "ws" + server.URL[4:] // Replace http with ws
|
||||
|
||||
// Create a client with test token
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-123",
|
||||
Title: "Test Device",
|
||||
DeviceType: proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP,
|
||||
AppName: "TestApp",
|
||||
AppVersion: "1.0.0",
|
||||
CanBePlayer: true,
|
||||
CanBeRemote: true,
|
||||
VolumeGran: 100,
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
// Override the wsURL for testing
|
||||
ynisonClient.overrideWSURL(wsURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ynisonClient.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
|
||||
if !ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be connected after Connect")
|
||||
}
|
||||
|
||||
// Wait a bit for the initial message to be sent
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify that an UpdateFullState message was sent
|
||||
if len(receivedMessages) == 0 {
|
||||
t.Error("Expected at least one message to be sent")
|
||||
} else {
|
||||
var req proto.PutYnisonStateRequest
|
||||
if err := json.Unmarshal(receivedMessages[0], &req); err != nil {
|
||||
t.Fatalf("Failed to unmarshal sent message: %v", err)
|
||||
}
|
||||
if req.UpdateFullState == nil {
|
||||
t.Error("Expected UpdateFullState in first message")
|
||||
}
|
||||
if req.UpdateFullState.Device == nil {
|
||||
t.Error("Expected Device in UpdateFullState")
|
||||
}
|
||||
if req.UpdateFullState.Device.Info.DeviceId != "test-device-123" {
|
||||
t.Errorf("Expected DeviceId 'test-device-123', got '%s'", req.UpdateFullState.Device.Info.DeviceId)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect failed: %v", err)
|
||||
}
|
||||
|
||||
if ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be disconnected after Disconnect")
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_Disconnect tests the Disconnect method
|
||||
func TestYnisonClient_Disconnect(t *testing.T) {
|
||||
// Create a mock WebSocket server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Keep connection open until client closes it
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
wsURL := "ws" + server.URL[4:]
|
||||
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-disconnect",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
ynisonClient.overrideWSURL(wsURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ynisonClient.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
|
||||
if !ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be connected")
|
||||
}
|
||||
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect failed: %v", err)
|
||||
}
|
||||
|
||||
if ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to be disconnected after Disconnect")
|
||||
}
|
||||
|
||||
// Calling Disconnect again should not error
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Second Disconnect should not error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_UpdatePlayingStatus tests the UpdatePlayingStatus method
|
||||
func TestYnisonClient_UpdatePlayingStatus(t *testing.T) {
|
||||
var receivedMessages [][]byte
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
receivedMessages = append(receivedMessages, message)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
wsURL := "ws" + server.URL[4:]
|
||||
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-status",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
ynisonClient.overrideWSURL(wsURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := ynisonClient.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Connect failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for initial connection message
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
initialMsgCount := len(receivedMessages)
|
||||
|
||||
// Create a playing status
|
||||
status := CreatePlayingStatus(50000, 200000, false, 1.0, "test-device-status")
|
||||
|
||||
// Update playing status
|
||||
err = ynisonClient.UpdatePlayingStatus(ctx, status)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePlayingStatus failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for message to be sent
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify the UpdatePlayingStatus message was sent
|
||||
if len(receivedMessages) <= initialMsgCount {
|
||||
t.Error("Expected UpdatePlayingStatus message to be sent")
|
||||
} else {
|
||||
var req proto.PutYnisonStateRequest
|
||||
if err := json.Unmarshal(receivedMessages[initialMsgCount], &req); err != nil {
|
||||
t.Fatalf("Failed to unmarshal sent message: %v", err)
|
||||
}
|
||||
if req.UpdatePlayingStatus == nil {
|
||||
t.Error("Expected UpdatePlayingStatus in message")
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus == nil {
|
||||
t.Error("Expected PlayingStatus in UpdatePlayingStatus")
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.ProgressMs != 50000 {
|
||||
t.Errorf("Expected ProgressMs 50000, got %d", req.UpdatePlayingStatus.PlayingStatus.ProgressMs)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.DurationMs != 200000 {
|
||||
t.Errorf("Expected DurationMs 200000, got %d", req.UpdatePlayingStatus.PlayingStatus.DurationMs)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.Paused != false {
|
||||
t.Errorf("Expected Paused false, got %v", req.UpdatePlayingStatus.PlayingStatus.Paused)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.PlaybackSpeed != 1.0 {
|
||||
t.Errorf("Expected PlaybackSpeed 1.0, got %f", req.UpdatePlayingStatus.PlayingStatus.PlaybackSpeed)
|
||||
}
|
||||
if req.UpdatePlayingStatus.PlayingStatus.Version.DeviceId != "test-device-status" {
|
||||
t.Errorf("Expected DeviceId 'test-device-status', got '%s'", req.UpdatePlayingStatus.PlayingStatus.Version.DeviceId)
|
||||
}
|
||||
if req.Rid == "" {
|
||||
t.Error("Expected non-empty Rid")
|
||||
}
|
||||
}
|
||||
|
||||
err = ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_UpdatePlayingStatus_NotConnected tests UpdatePlayingStatus when not connected
|
||||
func TestYnisonClient_UpdatePlayingStatus_NotConnected(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-not-connected",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
status := CreatePlayingStatus(50000, 200000, false, 1.0, "test-device-not-connected")
|
||||
|
||||
err := ynisonClient.UpdatePlayingStatus(context.Background(), status)
|
||||
if err == nil {
|
||||
t.Error("Expected error when not connected")
|
||||
}
|
||||
if err != nil && err.Error() != "not connected" {
|
||||
t.Errorf("Expected 'not connected' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_Connect_NoToken tests Connect without a token
|
||||
func TestYnisonClient_Connect_NoToken(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-no-token",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
err := ynisonClient.Connect(context.Background())
|
||||
if err == nil {
|
||||
t.Error("Expected error when no token")
|
||||
}
|
||||
if err != nil && err.Error() != "no auth token available" {
|
||||
t.Errorf("Expected 'no auth token available' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYnisonClient_IsConnected tests the IsConnected method
|
||||
func TestYnisonClient_IsConnected(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-isconnected",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
if ynisonClient.IsConnected() {
|
||||
t.Error("Expected client to not be connected initially")
|
||||
}
|
||||
|
||||
// We can't easily test connected state without a real server,
|
||||
// but we verified this in the Connect/Disconnect tests
|
||||
}
|
||||
|
||||
// TestYnisonClient_RegisterHandler tests handler registration
|
||||
func TestYnisonClient_RegisterHandler(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-handler",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
ynisonClient.RegisterHandler("test", func(resp *proto.PutYnisonStateResponse) {})
|
||||
|
||||
// We can't easily test handler invocation without a real server,
|
||||
// but we can verify the handler is registered
|
||||
ynisonClient.UnregisterHandler("test")
|
||||
|
||||
// Re-register and verify it doesn't panic
|
||||
ynisonClient.RegisterHandler("test2", func(resp *proto.PutYnisonStateResponse) {})
|
||||
ynisonClient.UnregisterHandler("test2")
|
||||
ynisonClient.UnregisterHandler("nonexistent") // Should not panic
|
||||
}
|
||||
|
||||
// TestYnisonClient_DisconnectWithoutConnect tests Disconnect without prior Connect
|
||||
func TestYnisonClient_DisconnectWithoutConnect(t *testing.T) {
|
||||
c := client.NewClient(client.ClientConfig{
|
||||
Token: "test-token",
|
||||
})
|
||||
|
||||
config := DeviceConfig{
|
||||
DeviceID: "test-device-disconnect-no-connect",
|
||||
}
|
||||
|
||||
ynisonClient := NewYnisonClient(c, config)
|
||||
|
||||
// Should not panic or error
|
||||
err := ynisonClient.Disconnect()
|
||||
if err != nil {
|
||||
t.Errorf("Disconnect without connect should not error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package ynison
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto"
|
||||
)
|
||||
|
||||
// CreatePlayingStatus creates a playing status
|
||||
func CreatePlayingStatus(progressMs, durationMs int64, paused bool, playbackSpeed float64, deviceID string) *proto.PlayingStatus {
|
||||
return &proto.PlayingStatus{
|
||||
ProgressMs: progressMs,
|
||||
DurationMs: durationMs,
|
||||
Paused: paused,
|
||||
PlaybackSpeed: playbackSpeed,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePlayable creates a playable entity
|
||||
func CreatePlayable(playableID, albumID, from, title, coverURL string, playableType proto.PlayablePlayableType) *proto.Playable {
|
||||
return &proto.Playable{
|
||||
PlayableId: playableID,
|
||||
AlbumIdOptional: albumID,
|
||||
PlayableType: playableType,
|
||||
From: from,
|
||||
Title: title,
|
||||
CoverUrlOptional: coverURL,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTrackPlayable creates a track playable
|
||||
func CreateTrackPlayable(playableID, albumID, from, title, coverURL string, trackSourceKey uint32, batchID string) *proto.Playable {
|
||||
return &proto.Playable{
|
||||
PlayableId: playableID,
|
||||
AlbumIdOptional: albumID,
|
||||
PlayableType: proto.PlayablePlayableType_PLAYABLE_TYPE_TRACK,
|
||||
From: from,
|
||||
Title: title,
|
||||
CoverUrlOptional: coverURL,
|
||||
TrackInfo: &proto.TrackInfo{
|
||||
TrackSourceKey: trackSourceKey,
|
||||
BatchIdOptional: batchID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVideoClipPlayable creates a video clip playable
|
||||
func CreateVideoClipPlayable(playableID, from, title, coverURL string, recommendationType proto.VideoClipInfoRecommendationType) *proto.Playable {
|
||||
return &proto.Playable{
|
||||
PlayableId: playableID,
|
||||
PlayableType: proto.PlayablePlayableType_PLAYABLE_TYPE_VIDEO_CLIP,
|
||||
From: from,
|
||||
Title: title,
|
||||
CoverUrlOptional: coverURL,
|
||||
VideoClipInfo: &proto.VideoClipInfo{
|
||||
RecommendationType: recommendationType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePlayerQueue creates a player queue
|
||||
func CreatePlayerQueue(entityID string, entityType proto.PlayerQueueEntityType, playables []*proto.Playable, currentIndex int32, deviceID string) *proto.PlayerQueue {
|
||||
return &proto.PlayerQueue{
|
||||
EntityId: entityID,
|
||||
EntityType: entityType,
|
||||
CurrentPlayableIndex: currentIndex,
|
||||
PlayableList: playables,
|
||||
Options: &proto.PlayerStateOptions{
|
||||
RepeatMode: proto.PlayerStateOptionsRepeatMode_REPEAT_MODE_NONE,
|
||||
},
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
EntityContext: proto.PlayerQueueEntityContext_ENTITY_CONTEXT_BASED_ON_ENTITY_BY_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWaveQueue creates a wave queue
|
||||
func CreateWaveQueue(recommended []*proto.Playable, liveIndex int32, sessionID string, deviceID string) *proto.PlayerQueueQueue {
|
||||
// Build track sources from playables
|
||||
var trackSources []*proto.PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey
|
||||
for i := range recommended {
|
||||
trackSources = append(trackSources, &proto.PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey{
|
||||
Key: uint32(i + 1),
|
||||
WaveSource: &proto.PlayerQueueQueueWaveQueueEntityOptionsWaveSource{
|
||||
SourceType: proto.WaveSourceSourceType_WAVE_SOURCE_ONLINE_BY_DEFAULT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &proto.PlayerQueueQueue{
|
||||
WaveQueue: &proto.PlayerQueueQueueWaveQueue{
|
||||
RecommendedPlayableList: recommended,
|
||||
LivePlayableIndex: liveIndex,
|
||||
EntityOptions: &proto.PlayerQueueQueueWaveQueueEntityOptions{
|
||||
WaveEntityOptional: &proto.PlayerQueueQueueWaveQueueEntityOptionsWaveSession{
|
||||
SessionId: sessionID,
|
||||
},
|
||||
TrackSources: trackSources,
|
||||
},
|
||||
NavigationIdOptional: "",
|
||||
PlaybackActionIdOptional: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateGenerativeQueue creates a generative queue
|
||||
func CreateGenerativeQueue(id string) *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
GenerativeQueue: &proto.PlayerQueueQueueGenerativeQueue{
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFMRadioQueue creates an FM radio queue
|
||||
func CreateFMRadioQueue(id string) *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
FmRadioQueue: &proto.PlayerQueueQueueFmRadioQueue{
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVideoWaveQueue creates a video wave queue
|
||||
func CreateVideoWaveQueue(id string) *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
VideoWaveQueue: &proto.PlayerQueueQueueVideoWaveQueue{
|
||||
Id: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateLocalTracksQueue creates a local tracks queue
|
||||
func CreateLocalTracksQueue() *proto.PlayerQueueQueue {
|
||||
return &proto.PlayerQueueQueue{
|
||||
LocalTracksQueue: &proto.PlayerQueueQueueLocalTracksQueue{},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeviceInfo creates device info
|
||||
func CreateDeviceInfo(deviceID, title string, deviceType proto.DeviceType, appName, appVersion string) *proto.DeviceInfo {
|
||||
return &proto.DeviceInfo{
|
||||
DeviceId: deviceID,
|
||||
Title: title,
|
||||
Type: deviceType,
|
||||
AppName: appName,
|
||||
AppVersion: appVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeviceCapabilities creates device capabilities
|
||||
func CreateDeviceCapabilities(canBePlayer, canBeRemote bool, volumeGranularity uint32) *proto.DeviceCapabilities {
|
||||
return &proto.DeviceCapabilities{
|
||||
CanBePlayer: canBePlayer,
|
||||
CanBeRemoteController: canBeRemote,
|
||||
VolumeGranularity: volumeGranularity,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeviceVolume creates device volume
|
||||
func CreateDeviceVolume(volume float64, deviceID string) *proto.DeviceVolume {
|
||||
return &proto.DeviceVolume{
|
||||
Volume: volume,
|
||||
Version: &proto.UpdateVersion{
|
||||
DeviceId: deviceID,
|
||||
Version: 1,
|
||||
TimestampMs: time.Now().UnixMilli(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDevice creates a device
|
||||
func CreateDevice(deviceID, title string, deviceType proto.DeviceType, appName, appVersion string, volume float64) *proto.Device {
|
||||
return &proto.Device{
|
||||
Info: CreateDeviceInfo(deviceID, title, deviceType, appName, appVersion),
|
||||
Volume: volume,
|
||||
Capabilities: CreateDeviceCapabilities(true, true, 100),
|
||||
Session: &proto.Session{
|
||||
Id: time.Now().UnixNano(),
|
||||
},
|
||||
IsOffline: false,
|
||||
VolumeInfo: CreateDeviceVolume(volume, deviceID),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUpdateFullState creates an UpdateFullState request
|
||||
func CreateUpdateFullState(deviceID string, isActive bool, playerState *proto.PlayerState) *proto.UpdateFullState {
|
||||
return &proto.UpdateFullState{
|
||||
PlayerState: playerState,
|
||||
IsCurrentlyActive: isActive,
|
||||
Device: &proto.UpdateDevice{
|
||||
Info: CreateDeviceInfo(deviceID, "Go Ynison Client", proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP, "YandexMusicGo", "1.0.0"),
|
||||
Volume: 0.5,
|
||||
Capabilities: CreateDeviceCapabilities(true, true, 100),
|
||||
VolumeInfo: CreateDeviceVolume(0.5, deviceID),
|
||||
},
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,406 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package ynison;
|
||||
|
||||
option go_package = "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto";
|
||||
|
||||
// DeviceType represents the type of device
|
||||
enum DeviceType {
|
||||
DEVICE_TYPE_UNSPECIFIED = 0;
|
||||
DEVICE_TYPE_WEB = 1;
|
||||
DEVICE_TYPE_ANDROID = 2;
|
||||
DEVICE_TYPE_IOS = 3;
|
||||
DEVICE_TYPE_SMART_SPEAKER = 4;
|
||||
DEVICE_TYPE_WEB_TV = 5;
|
||||
DEVICE_TYPE_ANDROID_TV = 6;
|
||||
DEVICE_TYPE_APPLE_TV = 7;
|
||||
DEVICE_TYPE_ANDROID_WEAR = 8;
|
||||
DEVICE_TYPE_WEB_DESKTOP = 9;
|
||||
}
|
||||
|
||||
// PlayablePlayableType represents the type of playable entity
|
||||
enum PlayablePlayableType {
|
||||
PLAYABLE_TYPE_UNSPECIFIED = 0;
|
||||
PLAYABLE_TYPE_TRACK = 1;
|
||||
PLAYABLE_TYPE_LOCAL_TRACK = 2;
|
||||
PLAYABLE_TYPE_INFINITE = 3;
|
||||
PLAYABLE_TYPE_VIDEO_CLIP = 4;
|
||||
}
|
||||
|
||||
// VideoClipInfoRecommendationType represents how a video clip got into the queue
|
||||
enum VideoClipInfoRecommendationType {
|
||||
VIDEO_CLIP_RECOMMENDATION_UNSPECIFIED = 0;
|
||||
VIDEO_CLIP_RECOMMENDATION_RECOMMENDED = 1;
|
||||
VIDEO_CLIP_RECOMMENDATION_ON_DEMAND = 2;
|
||||
VIDEO_CLIP_RECOMMENDATION_SEARCH = 3;
|
||||
VIDEO_CLIP_RECOMMENDATION_ARTIST = 4;
|
||||
VIDEO_CLIP_RECOMMENDATION_OWN = 5;
|
||||
VIDEO_CLIP_RECOMMENDATION_EDITORIAL_CHOICE = 6;
|
||||
}
|
||||
|
||||
// PlayerQueueEntityType represents the type of entity the queue originated from
|
||||
enum PlayerQueueEntityType {
|
||||
ENTITY_TYPE_UNSPECIFIED = 0;
|
||||
ENTITY_TYPE_ARTIST = 1;
|
||||
ENTITY_TYPE_PLAYLIST = 2;
|
||||
ENTITY_TYPE_ALBUM = 3;
|
||||
ENTITY_TYPE_RADIO = 4;
|
||||
ENTITY_TYPE_VARIOUS = 5;
|
||||
ENTITY_TYPE_GENERATIVE = 6;
|
||||
ENTITY_TYPE_FM_RADIO = 7;
|
||||
ENTITY_TYPE_VIDEO_WAVE = 8;
|
||||
ENTITY_TYPE_LOCAL_TRACKS = 9;
|
||||
}
|
||||
|
||||
// PlayerQueueEntityContext represents the playback context
|
||||
enum PlayerQueueEntityContext {
|
||||
ENTITY_CONTEXT_UNSPECIFIED = 0;
|
||||
ENTITY_CONTEXT_BASED_ON_ENTITY_BY_DEFAULT = 1;
|
||||
ENTITY_CONTEXT_USER_TRACKS = 2;
|
||||
ENTITY_CONTEXT_DOWNLOADED_TRACKS = 3;
|
||||
ENTITY_CONTEXT_SEARCH = 4;
|
||||
ENTITY_CONTEXT_MUSIC_HISTORY = 5;
|
||||
ENTITY_CONTEXT_MUSIC_HISTORY_SEARCH = 6;
|
||||
ENTITY_CONTEXT_ARTIST_MY_COLLECTION = 7;
|
||||
ENTITY_CONTEXT_ARTIST_FAMILIAR_FROM_WAVE = 8;
|
||||
}
|
||||
|
||||
// WaveSourceSourceType represents how a track got into the wave queue
|
||||
enum WaveSourceSourceType {
|
||||
WAVE_SOURCE_ONLINE_BY_DEFAULT = 0;
|
||||
WAVE_SOURCE_OFFLINE = 1;
|
||||
}
|
||||
|
||||
// PlayerStateOptionsRepeatMode represents the repeat mode
|
||||
enum PlayerStateOptionsRepeatMode {
|
||||
REPEAT_MODE_UNSPECIFIED = 0;
|
||||
REPEAT_MODE_NONE = 1;
|
||||
REPEAT_MODE_ONE = 2;
|
||||
REPEAT_MODE_ALL = 3;
|
||||
}
|
||||
|
||||
// PlayerQueueInjectPlayablePlayableType represents injectable playable type
|
||||
enum PlayerQueueInjectPlayablePlayableType {
|
||||
INJECT_PLAYABLE_TYPE_UNSPECIFIED = 0;
|
||||
INJECT_PLAYABLE_TYPE_ALICE_SHOT = 1;
|
||||
INJECT_PLAYABLE_TYPE_AD = 2;
|
||||
INJECT_PLAYABLE_TYPE_PREROLL = 3;
|
||||
}
|
||||
|
||||
// PutYnisonStateRequestActivityInterceptionType represents activity interception type
|
||||
enum PutYnisonStateRequestActivityInterceptionType {
|
||||
INTERCEPTION_DO_NOT_INTERCEPT_BY_DEFAULT = 0;
|
||||
INTERCEPTION_INTERCEPT_IF_NO_ONE_ACTIVE = 1;
|
||||
INTERCEPTION_INTERCEPT_EAGER = 2;
|
||||
}
|
||||
|
||||
// Playable represents a playable entity (track, video clip, etc.)
|
||||
message Playable {
|
||||
string playable_id = 1;
|
||||
string album_id_optional = 2;
|
||||
PlayablePlayableType playable_type = 3;
|
||||
string from = 4;
|
||||
string title = 5;
|
||||
string cover_url_optional = 6;
|
||||
VideoClipInfo video_clip_info = 7;
|
||||
TrackInfo track_info = 8;
|
||||
string navigation_id_optional = 9;
|
||||
string playback_action_id_optional = 10;
|
||||
}
|
||||
|
||||
// VideoClipInfo represents additional info about a video clip
|
||||
message VideoClipInfo {
|
||||
VideoClipInfoRecommendationType recommendation_type = 1;
|
||||
}
|
||||
|
||||
// TrackInfo represents additional info about a track
|
||||
message TrackInfo {
|
||||
uint32 track_source_key = 1;
|
||||
string batch_id_optional = 2;
|
||||
}
|
||||
|
||||
// UpdateVersion represents a version of changes
|
||||
message UpdateVersion {
|
||||
string device_id = 1;
|
||||
int64 version = 2;
|
||||
int64 timestamp_ms = 3;
|
||||
}
|
||||
|
||||
// PlayerQueue represents a playback queue
|
||||
message PlayerQueue {
|
||||
string entity_id = 1;
|
||||
PlayerQueueEntityType entity_type = 2;
|
||||
PlayerQueueQueue queue = 12;
|
||||
int32 current_playable_index = 3;
|
||||
repeated Playable playable_list = 4;
|
||||
PlayerStateOptions options = 5;
|
||||
UpdateVersion version = 6;
|
||||
Shuffle shuffle_optional = 7;
|
||||
PlayerQueueEntityContext entity_context = 8;
|
||||
string from_optional = 9;
|
||||
PlayerQueueInitialEntity initial_entity_optional = 10;
|
||||
PlayerQueuePlayerQueueOptions adding_options_optional = 11;
|
||||
string navigation_id_optional = 13;
|
||||
string filter_optional = 14;
|
||||
string playback_action_id_optional = 15;
|
||||
}
|
||||
|
||||
// PlayerQueueQueue represents hierarchical queue structure
|
||||
message PlayerQueueQueue {
|
||||
PlayerQueueQueueWaveQueue wave_queue = 1;
|
||||
PlayerQueueQueueGenerativeQueue generative_queue = 2;
|
||||
PlayerQueueQueueFmRadioQueue fm_radio_queue = 3;
|
||||
PlayerQueueQueueVideoWaveQueue video_wave_queue = 4;
|
||||
PlayerQueueQueueLocalTracksQueue local_tracks_queue = 5;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueue represents a wave queue
|
||||
message PlayerQueueQueueWaveQueue {
|
||||
repeated Playable recommended_playable_list = 1;
|
||||
int32 live_playable_index = 2;
|
||||
PlayerQueueQueueWaveQueueEntityOptions entity_options = 3;
|
||||
string navigation_id_optional = 4;
|
||||
string playback_action_id_optional = 5;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptions represents wave queue entity options
|
||||
message PlayerQueueQueueWaveQueueEntityOptions {
|
||||
PlayerQueueQueueWaveQueueEntityOptionsWaveSession wave_entity_optional = 1;
|
||||
repeated PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey track_sources = 2;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsWaveSession represents a wave session
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsWaveSession {
|
||||
string session_id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey represents a track source with key
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsTrackSourceWithKey {
|
||||
uint32 key = 1;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsWaveSource wave_source = 2;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsPhonotekaSource phonoteka_source = 3;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsWaveSource represents a wave source
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsWaveSource {
|
||||
WaveSourceSourceType source_type = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsPhonotekaSource represents a phonoteka source
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsPhonotekaSource {
|
||||
PlayerQueueEntityContext entity_context = 1;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsArtistId artist_id = 2;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsPlaylistId playlist_id = 3;
|
||||
PlayerQueueQueueWaveQueueEntityOptionsAlbumId album_id = 4;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsArtistId represents artist ID
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsArtistId {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsPlaylistId represents playlist ID
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsPlaylistId {
|
||||
string id = 1;
|
||||
string filter_optional = 2;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueWaveQueueEntityOptionsAlbumId represents album ID
|
||||
message PlayerQueueQueueWaveQueueEntityOptionsAlbumId {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueGenerativeQueue represents a generative queue
|
||||
message PlayerQueueQueueGenerativeQueue {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueFmRadioQueue represents an FM radio queue
|
||||
message PlayerQueueQueueFmRadioQueue {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueVideoWaveQueue represents a video wave queue
|
||||
message PlayerQueueQueueVideoWaveQueue {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
// PlayerQueueQueueLocalTracksQueue represents a local tracks queue
|
||||
message PlayerQueueQueueLocalTracksQueue {}
|
||||
|
||||
// PlayerQueueInitialEntity represents initial queue entity (deprecated)
|
||||
message PlayerQueueInitialEntity {
|
||||
string entity_id = 1;
|
||||
PlayerQueueEntityType entity_type = 2;
|
||||
}
|
||||
|
||||
// PlayerQueuePlayerQueueOptions represents queue options (deprecated)
|
||||
message PlayerQueuePlayerQueueOptions {
|
||||
PlayerQueuePlayerQueueOptionsRadioOptions radio_options = 1;
|
||||
}
|
||||
|
||||
// PlayerQueuePlayerQueueOptionsRadioOptions represents radio options
|
||||
message PlayerQueuePlayerQueueOptionsRadioOptions {
|
||||
string session_id = 1;
|
||||
}
|
||||
|
||||
// PlayerStateOptions represents player state options
|
||||
message PlayerStateOptions {
|
||||
PlayerStateOptionsRepeatMode repeat_mode = 1;
|
||||
}
|
||||
|
||||
// Shuffle represents shuffle settings
|
||||
message Shuffle {
|
||||
repeated uint32 playable_indices = 1;
|
||||
}
|
||||
|
||||
// Device represents a device
|
||||
message Device {
|
||||
DeviceInfo info = 1;
|
||||
double volume = 2;
|
||||
DeviceCapabilities capabilities = 3;
|
||||
Session session = 4;
|
||||
bool is_offline = 5;
|
||||
DeviceVolume volume_info = 6;
|
||||
}
|
||||
|
||||
// DeviceVolume represents device volume state
|
||||
message DeviceVolume {
|
||||
double volume = 1;
|
||||
UpdateVersion version = 2;
|
||||
}
|
||||
|
||||
// DeviceInfo represents device information
|
||||
message DeviceInfo {
|
||||
string device_id = 1;
|
||||
string title = 2;
|
||||
DeviceType type = 3;
|
||||
string app_name = 4;
|
||||
string app_version = 5;
|
||||
}
|
||||
|
||||
// DeviceCapabilities represents device capabilities
|
||||
message DeviceCapabilities {
|
||||
bool can_be_player = 1;
|
||||
bool can_be_remote_controller = 2;
|
||||
uint32 volume_granularity = 3;
|
||||
}
|
||||
|
||||
// Session represents a device session
|
||||
message Session {
|
||||
int64 id = 1;
|
||||
}
|
||||
|
||||
// PlayingStatus represents playback status
|
||||
message PlayingStatus {
|
||||
int64 progress_ms = 1;
|
||||
int64 duration_ms = 2;
|
||||
bool paused = 3;
|
||||
double playback_speed = 4;
|
||||
UpdateVersion version = 5;
|
||||
}
|
||||
|
||||
// PlayerQueueInject represents injected queue entity state
|
||||
message PlayerQueueInject {
|
||||
PlayingStatus playing_status = 1;
|
||||
PlayerQueueInjectPlayable playable = 2;
|
||||
UpdateVersion version = 3;
|
||||
}
|
||||
|
||||
// PlayerQueueInjectPlayable represents an injectable playable
|
||||
message PlayerQueueInjectPlayable {
|
||||
string playable_id = 1;
|
||||
PlayerQueueInjectPlayablePlayableType playable_type = 2;
|
||||
string title = 3;
|
||||
string cover_url = 4;
|
||||
}
|
||||
|
||||
// PlayerState represents player state
|
||||
message PlayerState {
|
||||
PlayingStatus status = 1;
|
||||
PlayerQueue player_queue = 2;
|
||||
PlayerQueueInject player_queue_inject_optional = 3;
|
||||
}
|
||||
|
||||
// PutYnisonStateRequest represents a state update request
|
||||
message PutYnisonStateRequest {
|
||||
UpdateFullState update_full_state = 1;
|
||||
UpdateActiveDevice update_active_device = 2;
|
||||
UpdatePlayingStatus update_playing_status = 3;
|
||||
UpdatePlayerState update_player_state = 4;
|
||||
UpdateVolume update_volume = 5;
|
||||
UpdatePlayerQueueInject update_player_queue_inject = 6;
|
||||
UpdateSessionParams update_session_params = 9;
|
||||
UpdateVolumeInfo update_volume_info = 11;
|
||||
SyncStateFromEOV sync_state_from_eov = 12;
|
||||
int64 player_action_timestamp_ms = 7;
|
||||
string rid = 8;
|
||||
PutYnisonStateRequestActivityInterceptionType activity_interception_type = 10;
|
||||
}
|
||||
|
||||
// PutYnisonStateResponse represents a state update response
|
||||
message PutYnisonStateResponse {
|
||||
PlayerState player_state = 1;
|
||||
repeated Device devices = 2;
|
||||
string active_device_id_optional = 3;
|
||||
int64 timestamp_ms = 4;
|
||||
string rid = 5;
|
||||
}
|
||||
|
||||
// UpdatePlayerQueueInject represents player queue inject update
|
||||
message UpdatePlayerQueueInject {
|
||||
PlayerQueueInject player_queue_inject = 1;
|
||||
}
|
||||
|
||||
// UpdateActiveDevice represents active device update
|
||||
message UpdateActiveDevice {
|
||||
string device_id_optional = 1;
|
||||
}
|
||||
|
||||
// UpdatePlayingStatus represents playing status update
|
||||
message UpdatePlayingStatus {
|
||||
PlayingStatus playing_status = 1;
|
||||
}
|
||||
|
||||
// UpdateVolume represents volume update (deprecated)
|
||||
message UpdateVolume {
|
||||
double volume = 1;
|
||||
string device_id = 2;
|
||||
}
|
||||
|
||||
// UpdateVolumeInfo represents volume info update
|
||||
message UpdateVolumeInfo {
|
||||
string device_id = 1;
|
||||
DeviceVolume volume_info = 2;
|
||||
}
|
||||
|
||||
// UpdatePlayerState represents player state update
|
||||
message UpdatePlayerState {
|
||||
PlayerState player_state = 1;
|
||||
}
|
||||
|
||||
// UpdateFullState represents full state update
|
||||
message UpdateFullState {
|
||||
PlayerState player_state = 1;
|
||||
bool is_currently_active = 2;
|
||||
UpdateDevice device = 3;
|
||||
SyncStateFromEOV sync_state_from_eov_optional = 4;
|
||||
}
|
||||
|
||||
// UpdateSessionParams represents session params update
|
||||
message UpdateSessionParams {
|
||||
bool mute_events_if_passive = 1;
|
||||
}
|
||||
|
||||
// UpdateDevice represents device update
|
||||
message UpdateDevice {
|
||||
DeviceInfo info = 1;
|
||||
double volume = 2;
|
||||
DeviceCapabilities capabilities = 3;
|
||||
DeviceVolume volume_info = 4;
|
||||
}
|
||||
|
||||
// SyncStateFromEOV represents sync state from EOV request
|
||||
message SyncStateFromEOV {
|
||||
string actual_queue_id = 1;
|
||||
}
|
||||
Reference in new issue
Block a user