Initial commit

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

No files matched your search

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