Compare commits

...
11 Commits
Author SHA1 Message Date
Mrixs dbf9a46bb2 Fix TrackShort.Lyrics type mismatch
TrackShort.Lyrics was SupplementLyrics but Python reference shows it should be TrackLyricsInfo
(has_available_sync_lyrics, has_available_text_lyrics fields)
2026-10-18 15:07:46 +03:00
Mrixs 73775a8526 Add remaining model types and client methods
Models added:
- Disclaimer, ForeignAgent (credits.go)
- ArtistConcerts (concert.go)
- Dashboard, StationResult, Station, AdParams, GenerativeStream,
  GenerativeStreamData, GenerativeStreamInfo (radio.go)
- FileInfo, FileDownloadInfo, TrackFullInfo, SimilarTracks, TrackTrailer (track.go)
- Removed duplicate AdParams from feed.go

Client methods added:
- artists.go: GetArtistBriefInfo, GetArtistSimilar, GetArtistLinks,
  GetArtistAlsoAlbums, GetArtistDiscographyAlbums, GetArtistSafeDirectAlbums,
  GetArtistTrackIds, GetArtistAbout, GetArtistDonation, GetArtistInfo,
  GetArtistSkeleton, GetArtistTrailer
- albums.go: GetAlbumSimilarEntities, GetAlbumTrailer
- clips.go: GetClipsDisclaimer
- concerts.go: GetArtistConcerts, GetConcertSkeleton
- radio.go: GetRotorStationsDashboard, GetRotorStationsList,
  GetRotorStationInfo, GetRotorStationStream, GetRotorStationSettings
- supplements.go: GetTracksCredits, GetClipsCredits
- queue.go: GetQueuesList
- tracks.go: GetTracksFileInfo, GetTracksSimilar, GetTrackTrailer, GetTrackFullInfo
2026-10-18 13:32:45 +03:00
Mrixs 49920d8908 Add missing Track, Album, Artist model types and client methods
Track: Added FileInfo, FileDownloadInfo, TrackFullInfo, SimilarTracks types
Album: Added AlbumSimilarEntities, AlbumTrailer, SimilarEntityItem types
Artist: Added ArtistSimilar, ArtistLinks, ArtistAbout, ArtistInfo, ArtistSkeleton,
       ArtistTrailerStatus, ArtistLink, Stats, SkeletonBlock types

Client methods added:
- artists.go: GetArtistBriefInfo, GetArtistSimilar, GetArtistLinks, GetArtistAlsoAlbums,
  GetArtistDiscographyAlbums, GetArtistSafeDirectAlbums, GetArtistTrackIds, GetArtistAbout,
  GetArtistDonation, GetArtistInfo, GetArtistSkeleton, GetArtistTrailer
- albums.go: GetAlbumSimilarEntities, GetAlbumTrailer
- tracks.go: GetTracksFileInfo, GetTracksSimilar, GetTrackTrailer, GetTrackFullInfo
2026-10-18 13:24:41 +03:00
Mrixs 9d3bcc4a9b Add missing model fields from Python audit
LandingList: Added type, type_for_from, title, id, new_releases, new_playlists, podcasts
New type: PlaylistID
Presaves: Added upcoming_albums, released_albums
Presaves: Added DeJSON method
QueueItem: Added id, context, modified fields (fixed duplicate context)
Clip: Fixed PreviewURI → PreviewURL field name
2026-10-18 13:18:24 +03:00
Mrixs 463e0bcd1a Add missing model fields from Python reference
Feed: Added can_get_more_events, pumpkin, is_wizard_passed, headlines, today, next_revision
Label: Added description_formatted, image, links ([]Link), type
Landing: Added pumpkin, content_id
Pin: Added content_restrictions
Search: Added search_request_id, text, best, podcasts, podcast_episodes, clips, banner, page, per_page, misspell fields

Clip: Rewrote with all missing fields (version, player_id, uuid, thumbnail, track_ids, disclaimers, explicit, cover, content_restrictions)
Concert: Added datetime, afisha_url, city, place, address, content_rating, data_session_id, event_info
2026-10-18 13:14:15 +03:00
Mrixs bc026d82b4 Add missing Track and Album model fields from Python reference
Track: Added 50+ missing fields (storage_dir, file_size, real_id, og_image, type,
normalization, error, can_publish, state, filename, user_info, meta_data, regions,
content_warning, preview_duration_ms, remember_position, background_video_uri,
short_description, is_suitable_for_children, track_source, available_for_options,
derived_colors, fade, smart_preview_params, special_audio_resources, disclaimers,
background_video_id, player_id, mix_fade, chart, clip_ids, isrc, ugc_artist_name,
podcast_episode_type, pub_date, and more)

New types: Normalization, MetaData, Fade, SmartPreviewParams, CoverDerivedColors,
CustomWave, TrackId, TrackFullInfo, SimilarTracks, TrackTrailer

Album: Added 40+ missing fields (error, version, original_release_year, text_color,
short_description, is_premiere, is_banner, meta_type, storage_dir, og_image, buy,
recent, very_important, bests, duplicates, prerolls, regions, available_as_rbt,
lyrics_available, remember_position, duration_ms, explicit, start_date, likes_count,
deprecation, available_regions, available_for_options, disclaimers, action_button,
cover, derived_colors, meta_tag_id, child_content, background_image_url, duration_sec,
has_trailer, pager, sort_order)

New types: Deprecation, AlbumActionButton
2026-10-18 13:12:13 +03:00
Mrixs a9be10b5e5 Fix remaining type mismatches vs Python reference
- RotorSettings: Diversity float64→string, Energy float64→int, add Mood int and MoodEnergy string
- Ratings: Rating float64→int, add Week int and Day int
- MetatagArtistEntry: Remove Score float64 (not in Python), add PopularTracks []Track
- MusicHistoryItemData: Remove Progress float64 (not in Python)
2026-10-18 12:57:21 +03:00
Mrixs 577c4a418c Fix Artist model to match Python reference
- Link field removed (not in Python Artist model)
- Description: string → *Description object (text, uri fields)
- SocialLinks []SocialLink → Links []Link (title, href, type, social_network)
- Donations *ArtistDonations → DonationInfo *ArtistDonationInfo (title, url)
- Decomposed []Artist → []Decomposed (accepts string or Artist object)
- Added Decomposed, Link, Description, ArtistDonationInfo types
2026-10-18 12:44:17 +03:00
Mrixs 277a3e6924 Fix Album model to match Python reference
- Labels: changed from []Label to []LabelOrString (API returns both objects and strings)
- Trailer: changed from *TrailerInfo to *AlbumTrailerStatus (Python has AlbumTrailerStatus with only 'available' field)
- Added LabelOrString type with custom UnmarshalJSON
- Added AlbumTrailerStatus type with Available field
2026-10-18 12:39:45 +03:00
Mrixs 6312c24b0f Fix Track model to match Python reference
- R128: changed from JSONR128 float64 to R128 struct with i, tp, important_secs fields
  (Python R128 is an object, not a number)
- StorageID -> StorageDir with json tag storage_dir (Python API uses storage_dir)
- Lyrics: replaced TrackLyricsInfo with Lyrics (from supplement/lyrics.py) and
  TrackLyricsInfo (from track/lyrics_info.py with has_available_sync_lyrics,
  has_available_text_lyrics fields)
- Track.Lyrics field now maps to lyrics_info JSON key with TrackLyricsInfo type
2026-10-18 12:34:58 +03:00
Mrixs ad83d84531 Fix GetTrackDownloadInfo to return signed download URLs
- Pass quality parameter to /tracks/{track_id}/download-info API request
- Add GetTrackDownloadURL method that resolves signed download URLs by
  downloading XML from download_info_url, parsing host/path/ts/s,
  computing MD5 signature (salt: XGRlBW9FXlekgbPrRHuSiA), and
  returning https://{host}/get-mp3/{sign}/{ts}{path}
- Add DownloadInfoURL field to DownloadInfo model for the XML URL
- Update DownloadTrack to use GetDirectLink resolution when URL is empty
- Port signing logic from yandex-music-api-python download_info.py
2026-10-18 12:13:54 +03:00
28 changed files with 2591 additions and 201 deletions

No files matched your search

+21
View File
@@ -3,6 +3,7 @@ package client
import ( import (
"context" "context"
"fmt"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
@@ -69,3 +70,23 @@ func (c *Client) GetAlbumsWithTracks(ctx context.Context, albumIDs []string) ([]
} }
return albums, nil return albums, nil
} }
// GetAlbumSimilarEntities gets similar entities for an album (GET /albums/{id}/similar-entities).
func (c *Client) GetAlbumSimilarEntities(ctx context.Context, albumID string) (*models.AlbumSimilarEntities, error) {
var result models.AlbumSimilarEntities
if err := c.request.Get(ctx, fmt.Sprintf("/albums/%s/similar-entities", albumID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetAlbumTrailer gets album trailer (GET /albums/{id}/trailer).
func (c *Client) GetAlbumTrailer(ctx context.Context, albumID string) (*models.AlbumTrailer, error) {
var result models.AlbumTrailer
if err := c.request.Get(ctx, fmt.Sprintf("/albums/%s/trailer", albumID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
+138
View File
@@ -77,3 +77,141 @@ func (c *Client) GetArtistTracks(ctx context.Context, artistID string, page, pag
result.SetClient(c) result.SetClient(c)
return &result, nil return &result, nil
} }
// GetArtistBriefInfo gets brief info about an artist (GET /artists/{artist_id}/brief-info).
func (c *Client) GetArtistBriefInfo(ctx context.Context, artistID string) (*models.ArtistBriefInfo, error) {
var result models.ArtistBriefInfo
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/brief-info", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistSimilar gets similar artists (GET /artists/{artist_id}/similar).
func (c *Client) GetArtistSimilar(ctx context.Context, artistID string) (*models.ArtistSimilar, error) {
var result models.ArtistSimilar
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/similar", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistLinks gets links to artist's pages (GET /artists/{artist_id}/artist-links).
func (c *Client) GetArtistLinks(ctx context.Context, artistID string) (*models.ArtistLinks, error) {
var result models.ArtistLinks
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/artist-links", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistAlsoAlbums gets albums where artist participated (GET /artists/{artist_id}/also-albums).
func (c *Client) GetArtistAlsoAlbums(ctx context.Context, artistID string, page, pageSize int, sortBy string) (*models.ArtistAlbums, error) {
params := map[string]string{
"page": fmt.Sprintf("%d", page),
"page-size": fmt.Sprintf("%d", pageSize),
"sort-by": sortBy,
}
var result models.ArtistAlbums
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/also-albums", artistID), params, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistDiscographyAlbums gets artist discography (GET /artists/{artist_id}/discography-albums).
func (c *Client) GetArtistDiscographyAlbums(ctx context.Context, artistID string, page, pageSize int, sortBy string) (*models.ArtistAlbums, error) {
params := map[string]string{
"page": fmt.Sprintf("%d", page),
"page-size": fmt.Sprintf("%d", pageSize),
"sort-by": sortBy,
}
var result models.ArtistAlbums
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/discography-albums", artistID), params, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistSafeDirectAlbums gets safe direct albums (GET /artists/{artist_id}/safe-direct-albums).
func (c *Client) GetArtistSafeDirectAlbums(ctx context.Context, artistID string, sortBy, sortOrder string, limit int) (*models.ArtistAlbums, error) {
params := map[string]string{
"sort-by": sortBy,
"sort-order": sortOrder,
"limit": fmt.Sprintf("%d", limit),
}
var result models.ArtistAlbums
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/safe-direct-albums", artistID), params, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistTrackIds gets track IDs by artist (GET /artists/{artist_id}/track-ids).
func (c *Client) GetArtistTrackIds(ctx context.Context, artistID string, page, pageSize int) ([]string, error) {
params := map[string]string{
"page": fmt.Sprintf("%d", page),
"page-size": fmt.Sprintf("%d", pageSize),
}
var result []string
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/track-ids", artistID), params, &result); err != nil {
return nil, err
}
return result, nil
}
// GetArtistAbout gets "About artist" info (GET /artists/{artist_id}/about-artist).
func (c *Client) GetArtistAbout(ctx context.Context, artistID string) (*models.ArtistAbout, error) {
var result models.ArtistAbout
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/about-artist", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistDonation gets artist donation info (GET /artists/{artist_id}/blocks/artist-donation).
func (c *Client) GetArtistDonation(ctx context.Context, artistID string) (*models.ArtistDonations, error) {
var result models.ArtistDonations
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/blocks/artist-donation", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistInfo gets detailed artist info (GET /artists/{artist_id}/info).
func (c *Client) GetArtistInfo(ctx context.Context, artistID string) (*models.ArtistInfo, error) {
var result models.ArtistInfo
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/info", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistSkeleton gets artist page skeleton (GET /artists/{artist_id}/skeletons/{skeleton_id}).
func (c *Client) GetArtistSkeleton(ctx context.Context, artistID, skeletonID string) (*models.ArtistSkeleton, error) {
var result models.ArtistSkeleton
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/skeletons/%s", artistID, skeletonID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetArtistTrailer gets artist trailer (GET /artists/{artist_id}/trailer).
func (c *Client) GetArtistTrailer(ctx context.Context, artistID string) (*models.ArtistTrailerStatus, error) {
var result models.ArtistTrailerStatus
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/trailer", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
+10
View File
@@ -52,6 +52,16 @@ func (c *Client) GetArtistClips(ctx context.Context, artistID string) (*models.A
return &result, nil return &result, nil
} }
// GetClipsDisclaimer gets clips disclaimer (GET /clips/{id}/disclaimer).
func (c *Client) GetClipsDisclaimer(ctx context.Context, clipID string) (*models.Disclaimer, error) {
var result models.Disclaimer
if err := c.request.Get(ctx, fmt.Sprintf("/clips/%s/disclaimer", clipID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetShots gets shots with pagination (GET /shots). // GetShots gets shots with pagination (GET /shots).
func (c *Client) GetShots(ctx context.Context, page, pageSize int) ([]*models.Shot, error) { func (c *Client) GetShots(ctx context.Context, page, pageSize int) ([]*models.Shot, error) {
params := map[string]string{ params := map[string]string{
+20
View File
@@ -63,3 +63,23 @@ func (c *Client) GetConcertTabs(ctx context.Context) (*models.ConcertTabConfig,
} }
return &result, nil return &result, nil
} }
// GetArtistConcerts gets concerts by artist (GET /artists/{id}/concerts).
func (c *Client) GetArtistConcerts(ctx context.Context, artistID string) (*models.ArtistConcerts, error) {
var result models.ArtistConcerts
if err := c.request.Get(ctx, fmt.Sprintf("/artists/%s/concerts", artistID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetConcertSkeleton gets concert skeleton (GET /concerts/{id}/skeletons/{skeleton_id}).
func (c *Client) GetConcertSkeleton(ctx context.Context, concertID, skeletonID string) (*models.ConcertSkeleton, error) {
var result models.ConcertSkeleton
if err := c.request.Get(ctx, fmt.Sprintf("/concerts/%s/skeletons/%s", concertID, skeletonID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
+15
View File
@@ -7,6 +7,21 @@ import (
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
) )
// GetQueuesList gets the queues list (GET /queues).
func (c *Client) GetQueuesList(ctx context.Context) ([]*models.QueueItem, error) {
var result []models.QueueItem
if err := c.request.Get(ctx, "/queues", nil, &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
}
// GetQueue gets the current queue // GetQueue gets the current queue
func (c *Client) GetQueue(ctx context.Context) (*models.Queue, error) { func (c *Client) GetQueue(ctx context.Context) (*models.Queue, error) {
var result models.Queue var result models.Queue
+58
View File
@@ -121,3 +121,61 @@ func (c *Client) RotorSessionFeedback(ctx context.Context, sessionID, trackID, f
var result map[string]any var result map[string]any
return c.request.Post(ctx, fmt.Sprintf("/rotor/station/%s/feedback", sessionID), params, &result) return c.request.Post(ctx, fmt.Sprintf("/rotor/station/%s/feedback", sessionID), params, &result)
} }
// GetRotorStationsDashboard gets rotor stations dashboard (GET /rotor/stations/dashboard).
func (c *Client) GetRotorStationsDashboard(ctx context.Context) (*models.Dashboard, error) {
var result models.Dashboard
if err := c.request.Get(ctx, "/rotor/stations/dashboard", nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetRotorStationsList gets rotor stations list (GET /rotor/stations/list).
func (c *Client) GetRotorStationsList(ctx context.Context) ([]*models.StationResult, error) {
var result []models.StationResult
if err := c.request.Get(ctx, "/rotor/stations/list", nil, &result); err != nil {
return nil, err
}
stations := make([]*models.StationResult, len(result))
for i := range result {
result[i].SetClient(c)
stations[i] = &result[i]
}
return stations, nil
}
// GetRotorStationInfo gets rotor station info (GET /rotor/station/{id}/info).
func (c *Client) GetRotorStationInfo(ctx context.Context, stationID string) (*models.RotorStation, error) {
var result models.RotorStation
if err := c.request.Get(ctx, fmt.Sprintf("/rotor/station/%s/info", stationID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetRotorStationStream gets rotor station stream (GET /rotor/station/{id}/stream).
func (c *Client) GetRotorStationStream(ctx context.Context, stationID string) (*models.GenerativeStream, error) {
var result models.GenerativeStream
if err := c.request.Get(ctx, fmt.Sprintf("/rotor/station/%s/stream", stationID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetRotorStationSettings gets rotor station settings (POST /rotor/station/{id}/settings3).
func (c *Client) GetRotorStationSettings(ctx context.Context, stationID string, moodEnergy, diversity, language string) (bool, error) {
params := map[string]string{
"mood-energy": moodEnergy,
"diversity": diversity,
"language": language,
}
var result bool
if err := c.request.Post(ctx, fmt.Sprintf("/rotor/station/%s/settings3", stationID), params, &result); err != nil {
return false, err
}
return result, nil
}
+20
View File
@@ -129,6 +129,26 @@ type Sign struct {
Value string Value string
} }
// GetTracksCredits gets track credits (GET /tracks/{id}/credits).
func (c *Client) GetTracksCredits(ctx context.Context, trackID string) (*models.Credits, error) {
var result models.Credits
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/credits", trackID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetClipsCredits gets clip credits (GET /clips/{id}/credits).
func (c *Client) GetClipsCredits(ctx context.Context, clipID string) (*models.Credits, error) {
var result models.Credits
if err := c.request.Get(ctx, fmt.Sprintf("/clips/%s/credits", clipID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetSignRequest creates a signature for a track request (exported for external use). // GetSignRequest creates a signature for a track request (exported for external use).
// It delegates to utils.GetSignRequest with the default signing key. // It delegates to utils.GetSignRequest with the default signing key.
func GetSignRequest(trackID string) Sign { func GetSignRequest(trackID string) Sign {
+144 -10
View File
@@ -3,12 +3,48 @@ package client
import ( import (
"context" "context"
"crypto/md5"
"encoding/xml"
"fmt" "fmt"
"strings"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
) )
const downloadSignSalt = "XGRlBW9FXlekgbPrRHuSiA"
// downloadInfoXML represents the XML response from download_info_url.
type downloadInfoXML struct {
Host string `xml:"host"`
Path string `xml:"path"`
Ts string `xml:"ts"`
S string `xml:"s"`
}
// resolveDownloadURL downloads the XML from downloadInfoURL, parses host/path/ts/s,
// computes the MD5 signature, and returns the signed download URL:
// https://{host}/get-mp3/{sign}/{ts}{path}
func resolveDownloadURL(ctx context.Context, downloadInfoURL string, ri models.RequestInterface) (string, error) {
data, err := ri.Download(ctx, downloadInfoURL)
if err != nil {
return "", err
}
var xmlData downloadInfoXML
if err := xml.Unmarshal(data, &xmlData); err != nil {
return "", fmt.Errorf("failed to parse download info XML: %w", err)
}
if xmlData.Path == "" || xmlData.S == "" {
return "", utils.NewYandexMusicError("invalid download info XML: missing path or s", utils.ErrBadRequest)
}
sign := fmt.Sprintf("%x", md5.Sum([]byte(downloadSignSalt+xmlData.Path[1:]+xmlData.S)))
return fmt.Sprintf("https://%s/get-mp3/%s/%s%s", xmlData.Host, sign, xmlData.Ts, xmlData.Path), nil
}
// GetTracks gets tracks by their IDs (GET /tracks). // GetTracks gets tracks by their IDs (GET /tracks).
// Returns a slice of Track pointers, or nil if no IDs are provided. // Returns a slice of Track pointers, or nil if no IDs are provided.
func (c *Client) GetTracks(ctx context.Context, trackIDs []string) ([]*models.Track, error) { func (c *Client) GetTracks(ctx context.Context, trackIDs []string) ([]*models.Track, error) {
@@ -49,11 +85,12 @@ func (c *Client) GetTrack(ctx context.Context, trackID string) (*models.Track, e
// GetTrackDownloadInfo gets download info for a track (GET /tracks/{track_id}/download-info). // GetTrackDownloadInfo gets download info for a track (GET /tracks/{track_id}/download-info).
// The quality parameter specifies the desired audio quality (e.g., "mp3"). // The quality parameter specifies the desired audio quality (e.g., "mp3").
func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality string) ([]models.DownloadInfo, error) { 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{ params := map[string]string{
"track_id": trackID, "track_id": trackID,
} }
if quality != "" {
params["quality"] = quality
}
var result []models.DownloadInfo var result []models.DownloadInfo
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil { if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil {
@@ -62,8 +99,45 @@ func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality stri
return result, nil return result, nil
} }
// GetTrackDownloadURL returns a signed direct download URL for a track.
// It fetches download info, finds a matching entry, downloads the XML from its
// download_info_url, parses host/path/ts/s, computes the MD5 signature, and
// returns https://{host}/get-mp3/{sign}/{ts}{path}.
func (c *Client) GetTrackDownloadURL(ctx context.Context, trackID, format string, bitrate int) (string, error) {
info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
if err != nil {
return "", err
}
// Find matching download info
var di *models.DownloadInfo
for i := range info {
if info[i].Codec == format && (bitrate == 0 || info[i].Bitrate == bitrate) {
di = &info[i]
break
}
}
if di == nil && len(info) > 0 {
di = &info[0]
}
if di == nil {
return "", utils.NewYandexMusicError("no download info available", utils.ErrNotFound)
}
if di.URL != "" {
return di.URL, nil
}
if di.DownloadInfoURL == "" {
return "", utils.NewYandexMusicError("no download URL available", utils.ErrNotFound)
}
return resolveDownloadURL(ctx, di.DownloadInfoURL, c.request)
}
// DownloadTrack downloads a track by ID. // DownloadTrack downloads a track by ID.
// It first fetches download info, finds a matching format/bitrate, then downloads the file content. // It first fetches download info, finds a matching format/bitrate, resolves the
// signed URL if needed, then downloads the file content.
func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitrate int) ([]byte, error) { func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitrate int) ([]byte, error) {
info, err := c.GetTrackDownloadInfo(ctx, trackID, format) info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
if err != nil { if err != nil {
@@ -71,25 +145,85 @@ func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitr
} }
// Find matching download info // Find matching download info
var url string var di *models.DownloadInfo
for _, di := range info { for i := range info {
if di.Codec == format && (bitrate == 0 || di.Bitrate == bitrate) { if info[i].Codec == format && (bitrate == 0 || info[i].Bitrate == bitrate) {
url = di.URL di = &info[i]
break break
} }
} }
if di == nil && len(info) > 0 {
if url == "" && len(info) > 0 { di = &info[0]
url = info[0].URL }
if di == nil {
return nil, utils.NewYandexMusicError("no download info available", utils.ErrNotFound)
} }
url := di.URL
if url == "" { if url == "" {
if di.DownloadInfoURL == "" {
return nil, utils.NewYandexMusicError("no download URL available", utils.ErrNotFound) return nil, utils.NewYandexMusicError("no download URL available", utils.ErrNotFound)
} }
url, err = resolveDownloadURL(ctx, di.DownloadInfoURL, c.request)
if err != nil {
return nil, err
}
}
return c.request.Download(ctx, url) return c.request.Download(ctx, url)
} }
// GetTracksFileInfo gets file info for a track (GET /get-file-info).
func (c *Client) GetTracksFileInfo(ctx context.Context, trackID, quality string, codecs []string, transport string) (*models.FileInfo, error) {
params := map[string]string{
"track_id": trackID,
"quality": quality,
}
if len(codecs) > 0 {
params["codecs"] = strings.Join(codecs, ",")
}
if transport != "" {
params["transport"] = transport
}
var result models.FileInfo
if err := c.request.Get(ctx, "/get-file-info", params, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetTracksSimilar gets similar tracks (GET /tracks/{track_id}/similar).
func (c *Client) GetTracksSimilar(ctx context.Context, trackID string) (*models.SimilarTracks, error) {
var result models.SimilarTracks
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/similar", trackID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetTrackTrailer gets track trailer (GET /tracks/{track_id}/trailer).
func (c *Client) GetTrackTrailer(ctx context.Context, trackID string) (*models.TrackTrailer, error) {
var result models.TrackTrailer
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/trailer", trackID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// GetTrackFullInfo gets full track info (GET /tracks/{track_id}/full-info).
func (c *Client) GetTrackFullInfo(ctx context.Context, trackID string) (*models.TrackFullInfo, error) {
var result models.TrackFullInfo
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/full-info", trackID), nil, &result); err != nil {
return nil, err
}
result.SetClient(c)
return &result, nil
}
// SearchResult represents search results containing tracks. // SearchResult represents search results containing tracks.
type SearchResult struct { type SearchResult struct {
Tracks *SearchTracksResult `json:"tracks"` Tracks *SearchTracksResult `json:"tracks"`
+12 -12
View File
@@ -30,7 +30,7 @@ func TestClient_GetTracks(t *testing.T) {
"id": "12345", "id": "12345",
"title": "Test Track", "title": "Test Track",
"duration_ms": 180000, "duration_ms": 180000,
"storage_id": "67890", "storage_dir": "67890",
"cover_uri": "https://example.com/cover.jpg", "cover_uri": "https://example.com/cover.jpg",
"genre": "rock", "genre": "rock",
"version": "album", "version": "album",
@@ -42,7 +42,7 @@ func TestClient_GetTracks(t *testing.T) {
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}], "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}], "albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
"major": {"id": "major1", "name": "Major Label", "description": "Big label"}, "major": {"id": "major1", "name": "Major Label", "description": "Big label"},
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true} "lyrics_info": {"has_available_sync_lyrics": true, "has_available_text_lyrics": true}
} }
] ]
}`), }`),
@@ -63,8 +63,8 @@ func TestClient_GetTracks(t *testing.T) {
if tr.Major == nil || tr.Major.Name != "Major Label" { if tr.Major == nil || tr.Major.Name != "Major Label" {
t.Errorf("Expected Major 'Major Label'") t.Errorf("Expected Major 'Major Label'")
} }
if tr.Lyrics == nil || tr.Lyrics.Text != "Test lyrics" { if tr.LyricsInfo == nil || tr.LyricsInfo.HasAvailableSyncLyrics != true {
t.Errorf("Expected Lyrics 'Test lyrics'") t.Errorf("Expected LyricsInfo.HasAvailableSyncLyrics true")
} }
}, },
}, },
@@ -73,8 +73,8 @@ func TestClient_GetTracks(t *testing.T) {
trackIDs: []string{"12345", "67890"}, trackIDs: []string{"12345", "67890"},
mockResponse: []byte(`{ mockResponse: []byte(`{
"result": [ "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": "12345", "title": "Track 1", "duration_ms": 100000, "storage_dir": "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": []} {"id": "67890", "title": "Track 2", "duration_ms": 200000, "storage_dir": "2", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []}
] ]
}`), }`),
expectedTracks: 2, expectedTracks: 2,
@@ -189,7 +189,7 @@ func TestClient_GetTrack(t *testing.T) {
"id": "12345", "id": "12345",
"title": "Test Track", "title": "Test Track",
"duration_ms": 180000, "duration_ms": 180000,
"storage_id": "67890", "storage_dir": "67890",
"cover_uri": "https://example.com/cover.jpg", "cover_uri": "https://example.com/cover.jpg",
"genre": "rock", "genre": "rock",
"version": "album", "version": "album",
@@ -201,7 +201,7 @@ func TestClient_GetTrack(t *testing.T) {
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}], "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}], "albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
"major": {"id": "major1", "name": "Major Label", "description": "Big label"}, "major": {"id": "major1", "name": "Major Label", "description": "Big label"},
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true} "lyrics_info": {"has_available_sync_lyrics": true, "has_available_text_lyrics": true}
} }
] ]
}`), }`),
@@ -311,7 +311,7 @@ func TestClient_SearchTracks(t *testing.T) {
"id": "1", "id": "1",
"title": "Track 1", "title": "Track 1",
"duration_ms": 100000, "duration_ms": 100000,
"storage_id": "1", "storage_dir": "1",
"cover_uri": "", "cover_uri": "",
"genre": "", "genre": "",
"version": "", "version": "",
@@ -327,7 +327,7 @@ func TestClient_SearchTracks(t *testing.T) {
"id": "2", "id": "2",
"title": "Track 2", "title": "Track 2",
"duration_ms": 200000, "duration_ms": 200000,
"storage_id": "2", "storage_dir": "2",
"cover_uri": "", "cover_uri": "",
"genre": "", "genre": "",
"version": "", "version": "",
@@ -408,7 +408,7 @@ func TestClient_SearchTracks_Mocked(t *testing.T) {
"id": "1", "id": "1",
"title": "Track 1", "title": "Track 1",
"duration_ms": 100000, "duration_ms": 100000,
"storage_id": "1", "storage_dir": "1",
"cover_uri": "", "cover_uri": "",
"genre": "", "genre": "",
"version": "", "version": "",
@@ -424,7 +424,7 @@ func TestClient_SearchTracks_Mocked(t *testing.T) {
"id": "2", "id": "2",
"title": "Track 2", "title": "Track 2",
"duration_ms": 200000, "duration_ms": 200000,
"storage_id": "2", "storage_dir": "2",
"cover_uri": "", "cover_uri": "",
"genre": "", "genre": "",
"version": "", "version": "",
+267 -14
View File
@@ -9,30 +9,117 @@ import (
type Album struct { type Album struct {
BaseModel BaseModel
ID JSONID `json:"id"` ID JSONID `json:"id"`
Error string `json:"error"`
Title string `json:"title"` Title string `json:"title"`
CoverURI string `json:"cover_uri"`
Genre string `json:"genre"`
Year int `json:"year"`
TrackCount int `json:"track_count"` TrackCount int `json:"track_count"`
Available bool `json:"available"`
ReleaseDate string `json:"release_date"`
Artists []Artist `json:"artists"` Artists []Artist `json:"artists"`
Labels []Label `json:"labels"` Labels []LabelOrString `json:"labels"`
Volumes [][]Track `json:"volumes"` Available bool `json:"available"`
AvailableForPremiumUsers bool `json:"available_for_premium_users"`
Version string `json:"version"`
CoverURI string `json:"cover_uri"`
ContentWarning string `json:"content_warning"`
OriginalReleaseYear int `json:"original_release_year"`
Genre string `json:"genre"`
TextColor string `json:"text_color"`
ShortDescription string `json:"short_description"`
Description string `json:"description"` Description string `json:"description"`
IsPremiere bool `json:"is_premiere"`
IsBanner bool `json:"is_banner"`
MetaType string `json:"meta_type"`
StorageDir string `json:"storage_dir"`
OgImage string `json:"og_image"`
Buy []json.RawMessage `json:"buy"`
Recent bool `json:"recent"`
VeryImportant bool `json:"very_important"`
AvailableForMobile bool `json:"available_for_mobile"`
AvailablePartially bool `json:"available_partially"`
Bests []int `json:"bests"`
Duplicates []Album `json:"duplicates"`
Prerolls []json.RawMessage `json:"prerolls"`
Volumes [][]Track `json:"volumes"`
Year int `json:"year"`
ReleaseDate string `json:"release_date"`
Type string `json:"type"`
TrackPosition *TrackPosition `json:"track_position"`
Regions []string `json:"regions"`
AvailableAsRbt bool `json:"available_as_rbt"`
LyricsAvailable bool `json:"lyrics_available"`
RememberPosition bool `json:"remember_position"`
Albums []Album `json:"albums"`
DurationMs int `json:"duration_ms"`
Explicit bool `json:"explicit"`
StartDate string `json:"start_date"`
LikesCount int `json:"likes_count"`
Deprecation *Deprecation `json:"deprecation"`
AvailableRegions []string `json:"available_regions"`
AvailableForOptions []string `json:"available_for_options"`
ListeningFinished bool `json:"listening_finished"`
Disclaimers []string `json:"disclaimers"`
ActionButton *AlbumActionButton `json:"action_button"`
Cover *Cover `json:"cover"`
DerivedColors *CoverDerivedColors `json:"derived_colors"`
MetaTagID string `json:"meta_tag_id"`
ChildContent bool `json:"child_content"`
BackgroundImageURL string `json:"background_image_url"`
CustomWave *CustomWave `json:"custom_wave"`
DurationSec int `json:"duration_sec"`
HasTrailer bool `json:"has_trailer"`
Trailer *AlbumTrailerStatus `json:"trailer"`
Pager *Pager `json:"pager"`
SortOrder string `json:"sort_order"`
Meta *AlbumMeta `json:"meta"` Meta *AlbumMeta `json:"meta"`
Best *Best `json:"best"` Best *Best `json:"best"`
// Additional fields
Region string `json:"region"` Region string `json:"region"`
AvailableForPremiumUsers bool `json:"available_for_premium_users"`
AvailableForPremiumUsersOnly bool `json:"available_for_premium_users_only"` AvailableForPremiumUsersOnly bool `json:"available_for_premium_users_only"`
ContentWarning string `json:"content_warning"`
Label string `json:"label"` Label string `json:"label"`
CatalogNumber string `json:"catalog_number"` CatalogNumber string `json:"catalog_number"`
Options []string `json:"options"` Options []string `json:"options"`
TrackPositions []TrackPosition `json:"track_positions"` TrackPositions []TrackPosition `json:"track_positions"`
SimilarAlbums []Album `json:"similar_albums"` SimilarAlbums []Album `json:"similar_albums"`
Trailer *TrailerInfo `json:"trailer"` }
// LabelOrString represents a label that can be either a Label object or a string.
type LabelOrString struct {
Label *Label `json:"-"`
Str string `json:"-"`
IsStr bool `json:"-"`
}
// UnmarshalJSON implements json.Unmarshaler, accepting both Label objects and strings.
func (l *LabelOrString) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
l.Str = s
l.IsStr = true
return nil
}
var label Label
if err := json.Unmarshal(data, &label); err != nil {
return err
}
l.Label = &label
l.IsStr = false
return nil
}
// MarshalJSON implements json.Marshaler.
func (l LabelOrString) MarshalJSON() ([]byte, error) {
if l.IsStr {
return json.Marshal(l.Str)
}
return json.Marshal(l.Label)
}
// String returns the label as a string.
func (l LabelOrString) String() string {
if l.IsStr {
return l.Str
}
if l.Label != nil {
return l.Label.Name
}
return ""
} }
// DeJSON deserializes an Album from JSON data. // DeJSON deserializes an Album from JSON data.
@@ -48,6 +135,9 @@ func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) {
if v, ok := raw["id"]; ok { if v, ok := raw["id"]; ok {
json.Unmarshal(v, &album.ID) json.Unmarshal(v, &album.ID)
} }
if v, ok := raw["error"]; ok {
json.Unmarshal(v, &album.Error)
}
if v, ok := raw["title"]; ok { if v, ok := raw["title"]; ok {
json.Unmarshal(v, &album.Title) json.Unmarshal(v, &album.Title)
} }
@@ -101,10 +191,11 @@ func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) {
album.Artists = artists album.Artists = artists
} }
if v, ok := raw["labels"]; ok { if v, ok := raw["labels"]; ok {
var labels []Label var labels []LabelOrString
json.Unmarshal(v, &labels) if err := json.Unmarshal(v, &labels); err == nil {
album.Labels = labels album.Labels = labels
} }
}
if v, ok := raw["volumes"]; ok { if v, ok := raw["volumes"]; ok {
var volumes [][]Track var volumes [][]Track
json.Unmarshal(v, &volumes) json.Unmarshal(v, &volumes)
@@ -131,7 +222,7 @@ func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) {
album.SimilarAlbums = similar album.SimilarAlbums = similar
} }
if v, ok := raw["trailer"]; ok { if v, ok := raw["trailer"]; ok {
var trailer TrailerInfo var trailer AlbumTrailerStatus
json.Unmarshal(v, &trailer) json.Unmarshal(v, &trailer)
album.Trailer = &trailer album.Trailer = &trailer
} }
@@ -273,3 +364,165 @@ func (ti *TrailerInfo) DeList(data []byte, c ClientInterface) ([]Model, error) {
} }
return result, nil return result, nil
} }
// AlbumTrailerStatus represents trailer availability status for an album.
type AlbumTrailerStatus struct {
BaseModel
Available bool `json:"available"`
}
// DeJSON deserializes AlbumTrailerStatus from JSON data.
func (ats *AlbumTrailerStatus) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
status := &AlbumTrailerStatus{BaseModel: *NewBaseModel(c)}
if v, ok := raw["available"]; ok {
json.Unmarshal(v, &status.Available)
}
return status, nil
}
// DeList deserializes a list of AlbumTrailerStatus from JSON data.
func (ats *AlbumTrailerStatus) 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 {
status := &AlbumTrailerStatus{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, status); err != nil {
continue
}
result = append(result, status)
}
return result, nil
}
// Deprecation represents album deprecation info.
type Deprecation struct {
BaseModel
TargetAlbumID JSONID `json:"target_album_id"`
Status string `json:"status"`
Done bool `json:"done"`
}
// AlbumActionButton represents an action button for an album.
type AlbumActionButton struct {
BaseModel
Text string `json:"text"`
URL string `json:"url"`
Color string `json:"color"`
ViewBrowser bool `json:"view_browser"`
}
// AlbumSimilarEntities represents similar entities for an album.
type AlbumSimilarEntities struct {
BaseModel
Items []SimilarEntityItem `json:"items"`
}
// DeJSON deserializes AlbumSimilarEntities from JSON data.
func (ae *AlbumSimilarEntities) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
entities := &AlbumSimilarEntities{BaseModel: *NewBaseModel(c)}
if v, ok := raw["items"]; ok {
var items []SimilarEntityItem
json.Unmarshal(v, &items)
entities.Items = items
}
return entities, nil
}
// DeList deserializes a list of AlbumSimilarEntities from JSON data.
func (ae *AlbumSimilarEntities) 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 {
entities := &AlbumSimilarEntities{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, entities); err != nil {
continue
}
result = append(result, entities)
}
return result, nil
}
// SimilarEntityItem represents a similar entity item.
type SimilarEntityItem struct {
BaseModel
ID JSONID `json:"id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
// AlbumTrailer represents album trailer info.
type AlbumTrailer struct {
BaseModel
Album *Album `json:"album"`
Artists []Artist `json:"artists"`
Trailer *TrailerInfo `json:"trailer"`
}
// DeJSON deserializes AlbumTrailer from JSON data.
func (at *AlbumTrailer) 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 := &AlbumTrailer{BaseModel: *NewBaseModel(c)}
if v, ok := raw["album"]; ok {
var album Album
json.Unmarshal(v, &album)
trailer.Album = &album
}
if v, ok := raw["artists"]; ok {
var artists []Artist
json.Unmarshal(v, &artists)
trailer.Artists = artists
}
if v, ok := raw["trailer"]; ok {
var trailerInfo TrailerInfo
json.Unmarshal(v, &trailerInfo)
trailer.Trailer = &trailerInfo
}
return trailer, nil
}
// DeList deserializes a list of AlbumTrailer from JSON data.
func (at *AlbumTrailer) 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 := &AlbumTrailer{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, trailer); err != nil {
continue
}
result = append(result, trailer)
}
return result, nil
}
+18 -39
View File
@@ -30,7 +30,7 @@ func TestAlbum_DeJSON(t *testing.T) {
"best": {"tracks": [{"id": "bt1", "title": "Best Track", "duration_ms": 200000}]}, "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}], "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}], "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} "trailer": {"available": true}
}`) }`)
album := &Album{BaseModel: *NewBaseModel(client)} album := &Album{BaseModel: *NewBaseModel(client)}
@@ -101,8 +101,8 @@ func TestAlbum_DeJSON(t *testing.T) {
if len(resultAlbum.Labels) != 1 { if len(resultAlbum.Labels) != 1 {
t.Errorf("Expected 1 label, got %d", len(resultAlbum.Labels)) t.Errorf("Expected 1 label, got %d", len(resultAlbum.Labels))
} }
if resultAlbum.Labels[0].Name != "Label 1" { if resultAlbum.Labels[0].String() != "Label 1" {
t.Errorf("Expected label name 'Label 1', got '%s'", resultAlbum.Labels[0].Name) t.Errorf("Expected label name 'Label 1', got '%s'", resultAlbum.Labels[0].String())
} }
// Test volumes // Test volumes
@@ -164,17 +164,8 @@ func TestAlbum_DeJSON(t *testing.T) {
if resultAlbum.Trailer == nil { if resultAlbum.Trailer == nil {
t.Fatal("Expected Trailer to be non-nil") t.Fatal("Expected Trailer to be non-nil")
} }
if resultAlbum.Trailer.PreviewURI != "https://example.com/preview.mp3" { if !resultAlbum.Trailer.Available {
t.Errorf("Expected PreviewURI 'https://example.com/preview.mp3', got '%s'", resultAlbum.Trailer.PreviewURI) t.Errorf("Expected Trailer.Available true, got false")
}
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)
} }
} }
@@ -308,50 +299,38 @@ func TestAlbumMeta_DeList(t *testing.T) {
} }
} }
func TestTrailerInfo_DeJSON(t *testing.T) { func TestAlbumTrailerStatus_DeJSON(t *testing.T) {
client := newMockClient() client := newMockClient()
data := []byte(`{ data := []byte(`{
"preview_uri": "https://example.com/preview.mp3", "available": true
"track_id": "track123",
"album_id": "album456",
"duration": 45
}`) }`)
trailer := &TrailerInfo{BaseModel: *NewBaseModel(client)} status := &AlbumTrailerStatus{BaseModel: *NewBaseModel(client)}
result, err := trailer.DeJSON(data, client) result, err := status.DeJSON(data, client)
if err != nil { if err != nil {
t.Fatalf("DeJSON failed: %v", err) t.Fatalf("DeJSON failed: %v", err)
} }
resultTrailer := result.(*TrailerInfo) resultStatus := result.(*AlbumTrailerStatus)
if resultTrailer.PreviewURI != "https://example.com/preview.mp3" { if !resultStatus.Available {
t.Errorf("Expected PreviewURI 'https://example.com/preview.mp3', got '%s'", resultTrailer.PreviewURI) t.Errorf("Expected Available true, got false")
}
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) { func TestAlbumTrailerStatus_DeList(t *testing.T) {
client := newMockClient() client := newMockClient()
data := []byte(`[ data := []byte(`[
{"preview_uri": "https://example.com/preview1.mp3", "track_id": "t1", "album_id": "a1", "duration": 30}, {"available": true},
{"preview_uri": "https://example.com/preview2.mp3", "track_id": "t2", "album_id": "a2", "duration": 45} {"available": false}
]`) ]`)
trailer := &TrailerInfo{BaseModel: *NewBaseModel(client)} status := &AlbumTrailerStatus{BaseModel: *NewBaseModel(client)}
result, err := trailer.DeList(data, client) result, err := status.DeList(data, client)
if err != nil { if err != nil {
t.Fatalf("DeList failed: %v", err) t.Fatalf("DeList failed: %v", err)
} }
if len(result) != 2 { if len(result) != 2 {
t.Errorf("Expected 2 trailers, got %d", len(result)) t.Errorf("Expected 2 statuses, got %d", len(result))
} }
} }
+540 -21
View File
@@ -14,20 +14,19 @@ type Artist struct {
Genres []string `json:"genres"` Genres []string `json:"genres"`
Tracks int `json:"tracks"` Tracks int `json:"tracks"`
Albums int `json:"albums"` Albums int `json:"albums"`
Link string `json:"link"`
Available bool `json:"available"` Available bool `json:"available"`
Description string `json:"description"`
Composer bool `json:"composer"` Composer bool `json:"composer"`
Various bool `json:"various"` Various bool `json:"various"`
Decomposed []Artist `json:"decomposed"` Decomposed []Decomposed `json:"decomposed"`
Ratings *Ratings `json:"ratings"` Ratings *Ratings `json:"ratings"`
Counts *Counts `json:"counts"` Counts *Counts `json:"counts"`
SocialLinks []SocialLink `json:"social_links"` Links []Link `json:"social_links"`
Description *Description `json:"description"`
// Additional fields // Additional fields
IDForFrom string `json:"id_for_from"` IDForFrom string `json:"id_for_from"`
Cover *Cover `json:"cover"` Cover *Cover `json:"cover"`
BriefInfo *ArtistBriefInfo `json:"brief_info"` BriefInfo *ArtistBriefInfo `json:"brief_info"`
Donations *ArtistDonations `json:"donations"` DonationInfo *ArtistDonationInfo `json:"donation_info"`
Concerts []Concert `json:"concerts"` Concerts []Concert `json:"concerts"`
Clips []Clip `json:"clips"` Clips []Clip `json:"clips"`
Similar []Artist `json:"similar"` Similar []Artist `json:"similar"`
@@ -63,15 +62,9 @@ func (a *Artist) DeJSON(data []byte, c ClientInterface) (Model, error) {
if v, ok := raw["albums"]; ok { if v, ok := raw["albums"]; ok {
json.Unmarshal(v, &artist.Albums) json.Unmarshal(v, &artist.Albums)
} }
if v, ok := raw["link"]; ok {
json.Unmarshal(v, &artist.Link)
}
if v, ok := raw["available"]; ok { if v, ok := raw["available"]; ok {
json.Unmarshal(v, &artist.Available) json.Unmarshal(v, &artist.Available)
} }
if v, ok := raw["description"]; ok {
json.Unmarshal(v, &artist.Description)
}
if v, ok := raw["composer"]; ok { if v, ok := raw["composer"]; ok {
json.Unmarshal(v, &artist.Composer) json.Unmarshal(v, &artist.Composer)
} }
@@ -84,10 +77,11 @@ func (a *Artist) DeJSON(data []byte, c ClientInterface) (Model, error) {
// Nested objects // Nested objects
if v, ok := raw["decomposed"]; ok { if v, ok := raw["decomposed"]; ok {
var decomposed []Artist var decomposed []Decomposed
json.Unmarshal(v, &decomposed) if err := json.Unmarshal(v, &decomposed); err == nil {
artist.Decomposed = decomposed artist.Decomposed = decomposed
} }
}
if v, ok := raw["ratings"]; ok { if v, ok := raw["ratings"]; ok {
var ratings Ratings var ratings Ratings
json.Unmarshal(v, &ratings) json.Unmarshal(v, &ratings)
@@ -99,9 +93,10 @@ func (a *Artist) DeJSON(data []byte, c ClientInterface) (Model, error) {
artist.Counts = &counts artist.Counts = &counts
} }
if v, ok := raw["social_links"]; ok { if v, ok := raw["social_links"]; ok {
var links []SocialLink var links []Link
json.Unmarshal(v, &links) if err := json.Unmarshal(v, &links); err == nil {
artist.SocialLinks = links artist.Links = links
}
} }
if v, ok := raw["cover"]; ok { if v, ok := raw["cover"]; ok {
var cover Cover var cover Cover
@@ -113,10 +108,10 @@ func (a *Artist) DeJSON(data []byte, c ClientInterface) (Model, error) {
json.Unmarshal(v, &brief) json.Unmarshal(v, &brief)
artist.BriefInfo = &brief artist.BriefInfo = &brief
} }
if v, ok := raw["donations"]; ok { if v, ok := raw["donation_info"]; ok {
var donations ArtistDonations var donation ArtistDonationInfo
json.Unmarshal(v, &donations) json.Unmarshal(v, &donation)
artist.Donations = &donations artist.DonationInfo = &donation
} }
if v, ok := raw["concerts"]; ok { if v, ok := raw["concerts"]; ok {
var concerts []Concert var concerts []Concert
@@ -167,8 +162,10 @@ func (a *Artist) DeList(data []byte, c ClientInterface) ([]Model, error) {
// Ratings represents artist ratings. // Ratings represents artist ratings.
type Ratings struct { type Ratings struct {
Rating float64 `json:"rating"` Rating int `json:"rating"`
Votes int `json:"votes"` Votes int `json:"votes"`
Week int `json:"week"`
Day int `json:"day"`
} }
// Counts represents artist counts. // Counts represents artist counts.
@@ -196,12 +193,14 @@ type Cover struct {
// ArtistBriefInfo represents brief artist info. // ArtistBriefInfo represents brief artist info.
type ArtistBriefInfo struct { type ArtistBriefInfo struct {
BaseModel
Description string `json:"description"` Description string `json:"description"`
Available bool `json:"available"` Available bool `json:"available"`
} }
// ArtistDonations represents artist donations info. // ArtistDonations represents artist donations info.
type ArtistDonations struct { type ArtistDonations struct {
BaseModel
Items []DonationItem `json:"items"` Items []DonationItem `json:"items"`
Total int `json:"total"` Total int `json:"total"`
Goal int `json:"goal"` Goal int `json:"goal"`
@@ -219,6 +218,178 @@ type DonationItem struct {
Currency string `json:"currency"` Currency string `json:"currency"`
} }
// Decomposed represents a decomposed artist that can be either a string or an Artist object.
type Decomposed struct {
Artist *Artist `json:"-"`
Str string `json:"-"`
IsStr bool `json:"-"`
}
// UnmarshalJSON implements json.Unmarshaler, accepting both string and Artist object.
func (d *Decomposed) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
d.Str = s
d.IsStr = true
return nil
}
var artist Artist
if err := json.Unmarshal(data, &artist); err != nil {
return err
}
d.Artist = &artist
d.IsStr = false
return nil
}
// MarshalJSON implements json.Marshaler.
func (d Decomposed) MarshalJSON() ([]byte, error) {
if d.IsStr {
return json.Marshal(d.Str)
}
return json.Marshal(d.Artist)
}
// Link represents a link to an artist's official page or social network.
type Link struct {
BaseModel
Title string `json:"title"`
Href string `json:"href"`
Type string `json:"type"`
SocialNetwork string `json:"social_network"`
}
// DeJSON deserializes Link from JSON data.
func (l *Link) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
link := &Link{BaseModel: *NewBaseModel(c)}
if v, ok := raw["title"]; ok {
json.Unmarshal(v, &link.Title)
}
if v, ok := raw["href"]; ok {
json.Unmarshal(v, &link.Href)
}
if v, ok := raw["type"]; ok {
json.Unmarshal(v, &link.Type)
}
if v, ok := raw["social_network"]; ok {
json.Unmarshal(v, &link.SocialNetwork)
}
return link, nil
}
// DeList deserializes a list of Links from JSON data.
func (l *Link) 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 {
link := &Link{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, link); err != nil {
continue
}
result = append(result, link)
}
return result, nil
}
// Description represents artist description.
type Description struct {
BaseModel
Text string `json:"text"`
URI string `json:"uri"`
}
// DeJSON deserializes Description from JSON data.
func (d *Description) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
desc := &Description{BaseModel: *NewBaseModel(c)}
if v, ok := raw["text"]; ok {
json.Unmarshal(v, &desc.Text)
}
if v, ok := raw["uri"]; ok {
json.Unmarshal(v, &desc.URI)
}
return desc, nil
}
// DeList deserializes a list of Descriptions from JSON data.
func (d *Description) 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 {
desc := &Description{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, desc); err != nil {
continue
}
result = append(result, desc)
}
return result, nil
}
// ArtistDonationInfo represents artist donation info.
type ArtistDonationInfo struct {
BaseModel
Title string `json:"title"`
URL string `json:"url"`
}
// DeJSON deserializes ArtistDonationInfo from JSON data.
func (adi *ArtistDonationInfo) 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 := &ArtistDonationInfo{BaseModel: *NewBaseModel(c)}
if v, ok := raw["title"]; ok {
json.Unmarshal(v, &info.Title)
}
if v, ok := raw["url"]; ok {
json.Unmarshal(v, &info.URL)
}
return info, nil
}
// DeList deserializes a list of ArtistDonationInfo from JSON data.
func (adi *ArtistDonationInfo) 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 := &ArtistDonationInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
}
// ConcertShort represents a concert (minimal for artist). // ConcertShort represents a concert (minimal for artist).
type ConcertShort struct { type ConcertShort struct {
BaseModel BaseModel
@@ -366,3 +537,351 @@ func (c *ClipShort) DeList(data []byte, client ClientInterface) ([]Model, error)
} }
return result, nil return result, nil
} }
// ArtistSimilar represents similar artists.
type ArtistSimilar struct {
BaseModel
Artist *Artist `json:"artist"`
SimilarArtists []Artist `json:"similar_artists"`
}
// DeJSON deserializes ArtistSimilar from JSON data.
func (as *ArtistSimilar) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
similar := &ArtistSimilar{BaseModel: *NewBaseModel(c)}
if v, ok := raw["artist"]; ok {
var artist Artist
json.Unmarshal(v, &artist)
similar.Artist = &artist
}
if v, ok := raw["similar_artists"]; ok {
var artists []Artist
json.Unmarshal(v, &artists)
similar.SimilarArtists = artists
}
return similar, nil
}
// DeList deserializes a list of ArtistSimilar from JSON data.
func (as *ArtistSimilar) 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 {
similar := &ArtistSimilar{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, similar); err != nil {
continue
}
result = append(result, similar)
}
return result, nil
}
// ArtistLinks represents links to artist's official pages or social networks.
type ArtistLinks struct {
BaseModel
Links []Link `json:"links"`
}
// DeJSON deserializes ArtistLinks from JSON data.
func (al *ArtistLinks) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
links := &ArtistLinks{BaseModel: *NewBaseModel(c)}
if v, ok := raw["links"]; ok {
var linkList []Link
json.Unmarshal(v, &linkList)
links.Links = linkList
}
return links, nil
}
// DeList deserializes a list of ArtistLinks from JSON data.
func (al *ArtistLinks) 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 {
links := &ArtistLinks{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, links); err != nil {
continue
}
result = append(result, links)
}
return result, nil
}
// ArtistAbout represents information "About artist".
type ArtistAbout struct {
BaseModel
Artist *Artist `json:"artist"`
Stats *Stats `json:"stats"`
Description string `json:"description"`
Links []ArtistLink `json:"links"`
Covers []Cover `json:"covers"`
ArtistType string `json:"artist_type"`
}
// DeJSON deserializes ArtistAbout from JSON data.
func (aa *ArtistAbout) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
about := &ArtistAbout{BaseModel: *NewBaseModel(c)}
if v, ok := raw["artist"]; ok {
var artist Artist
json.Unmarshal(v, &artist)
about.Artist = &artist
}
if v, ok := raw["stats"]; ok {
var stats Stats
json.Unmarshal(v, &stats)
about.Stats = &stats
}
if v, ok := raw["description"]; ok {
json.Unmarshal(v, &about.Description)
}
if v, ok := raw["links"]; ok {
var linkList []ArtistLink
json.Unmarshal(v, &linkList)
about.Links = linkList
}
if v, ok := raw["covers"]; ok {
var covers []Cover
json.Unmarshal(v, &covers)
about.Covers = covers
}
if v, ok := raw["artist_type"]; ok {
json.Unmarshal(v, &about.ArtistType)
}
return about, nil
}
// DeList deserializes a list of ArtistAbout from JSON data.
func (aa *ArtistAbout) 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 {
about := &ArtistAbout{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, about); err != nil {
continue
}
result = append(result, about)
}
return result, nil
}
// ArtistInfo represents detailed artist info.
type ArtistInfo struct {
BaseModel
Artist *Artist `json:"artist"`
LikesCount int `json:"likes_count"`
Stats *Stats `json:"stats"`
Trailer *ArtistTrailerStatus `json:"trailer"`
Covers []Cover `json:"covers"`
Description string `json:"description"`
ArtistType string `json:"artist_type"`
}
// DeJSON deserializes ArtistInfo from JSON data.
func (ai *ArtistInfo) 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 := &ArtistInfo{BaseModel: *NewBaseModel(c)}
if v, ok := raw["artist"]; ok {
var artist Artist
json.Unmarshal(v, &artist)
info.Artist = &artist
}
if v, ok := raw["likes_count"]; ok {
json.Unmarshal(v, &info.LikesCount)
}
if v, ok := raw["stats"]; ok {
var stats Stats
json.Unmarshal(v, &stats)
info.Stats = &stats
}
if v, ok := raw["trailer"]; ok {
var trailer ArtistTrailerStatus
json.Unmarshal(v, &trailer)
info.Trailer = &trailer
}
if v, ok := raw["covers"]; ok {
var covers []Cover
json.Unmarshal(v, &covers)
info.Covers = covers
}
if v, ok := raw["description"]; ok {
json.Unmarshal(v, &info.Description)
}
if v, ok := raw["artist_type"]; ok {
json.Unmarshal(v, &info.ArtistType)
}
return info, nil
}
// DeList deserializes a list of ArtistInfo from JSON data.
func (ai *ArtistInfo) 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 := &ArtistInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
}
// ArtistSkeleton represents artist page skeleton.
type ArtistSkeleton struct {
BaseModel
ID string `json:"id"`
Title string `json:"title"`
Blocks []SkeletonBlock `json:"blocks"`
}
// DeJSON deserializes ArtistSkeleton from JSON data.
func (as *ArtistSkeleton) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
skeleton := &ArtistSkeleton{BaseModel: *NewBaseModel(c)}
if v, ok := raw["id"]; ok {
json.Unmarshal(v, &skeleton.ID)
}
if v, ok := raw["title"]; ok {
json.Unmarshal(v, &skeleton.Title)
}
if v, ok := raw["blocks"]; ok {
var blocks []SkeletonBlock
json.Unmarshal(v, &blocks)
skeleton.Blocks = blocks
}
return skeleton, nil
}
// DeList deserializes a list of ArtistSkeleton from JSON data.
func (as *ArtistSkeleton) 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 {
skeleton := &ArtistSkeleton{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, skeleton); err != nil {
continue
}
result = append(result, skeleton)
}
return result, nil
}
// ArtistTrailerStatus represents trailer availability for artist.
type ArtistTrailerStatus struct {
BaseModel
Available bool `json:"available"`
}
// DeJSON deserializes ArtistTrailerStatus from JSON data.
func (ats *ArtistTrailerStatus) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
status := &ArtistTrailerStatus{BaseModel: *NewBaseModel(c)}
if v, ok := raw["available"]; ok {
json.Unmarshal(v, &status.Available)
}
return status, nil
}
// DeList deserializes a list of ArtistTrailerStatus from JSON data.
func (ats *ArtistTrailerStatus) 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 {
status := &ArtistTrailerStatus{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, status); err != nil {
continue
}
result = append(result, status)
}
return result, nil
}
// ArtistLink represents a link for artist about page.
type ArtistLink struct {
BaseModel
Title string `json:"title"`
Subtitle string `json:"subtitle,omitempty"`
URL string `json:"url,omitempty"`
ImgURL string `json:"img_url,omitempty"`
}
// Stats represents artist stats.
type Stats struct {
BaseModel
Rating int `json:"rating"`
Votes int `json:"votes"`
Month int `json:"month"`
Week int `json:"week"`
Day int `json:"day"`
}
// SkeletonBlock represents a skeleton block.
type SkeletonBlock struct {
BaseModel
ID string `json:"id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
+53 -17
View File
@@ -111,33 +111,69 @@ func (id JSONID) String() string {
return string(id) return string(id)
} }
// JSONR128 is a flexible R128 loudness normalization value that accepts both JSON numbers and objects. // R128 represents EBU R 128 loudness normalization parameters.
// The Yandex Music API sometimes returns r128 as a number (e.g., -23.5) and sometimes // The Yandex Music API returns this either as a number (e.g., -23.5) or as an object
// as an object (e.g., {"value": -23.5}). JSONR128 accepts both and stores as float64. // with fields i, tp, important_secs.
type JSONR128 float64 type R128 struct {
BaseModel
I float64 `json:"i"`
Tp float64 `json:"tp"`
ImportantSecs int `json:"important_secs"`
}
// UnmarshalJSON implements json.Unmarshaler, accepting both JSON numbers and objects. // UnmarshalJSON implements json.Unmarshaler, accepting both JSON numbers and objects.
func (r *JSONR128) UnmarshalJSON(data []byte) error { func (r *R128) UnmarshalJSON(data []byte) error {
// Try float64 first // Try float64 first (API sometimes returns just a number)
var f float64 var f float64
if err := json.Unmarshal(data, &f); err == nil { if err := json.Unmarshal(data, &f); err == nil {
*r = JSONR128(f) r.I = f
r.Tp = 0
r.ImportantSecs = 0
return nil return nil
} }
// Try object with "value" key // Try object with fields i, tp, important_secs
var obj struct { var obj struct {
Value float64 `json:"value"` I float64 `json:"i"`
Tp float64 `json:"tp"`
ImportantSecs int `json:"important_secs"`
} }
if err := json.Unmarshal(data, &obj); err == nil { if err := json.Unmarshal(data, &obj); err != nil {
*r = JSONR128(obj.Value) return err
return nil
} }
// Default to 0 if neither format works r.I = obj.I
*r = 0 r.Tp = obj.Tp
r.ImportantSecs = obj.ImportantSecs
return nil return nil
} }
// Float64 returns the R128 value as a float64. // Float64 returns the integrated loudness (i) value as a float64.
func (r JSONR128) Float64() float64 { func (r R128) Float64() float64 {
return float64(r) return r.I
}
// DeJSON deserializes an R128 from JSON data.
func (r *R128) DeJSON(data []byte, c ClientInterface) (Model, error) {
r128 := &R128{BaseModel: *NewBaseModel(c)}
if err := r128.UnmarshalJSON(data); err != nil {
return nil, err
}
return r128, nil
}
// DeList deserializes a list of R128 from JSON data.
func (r *R128) 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 {
r128 := &R128{BaseModel: *NewBaseModel(c)}
if err := r128.UnmarshalJSON(item); err != nil {
continue
}
result = append(result, r128)
}
return result, nil
} }
+45 -37
View File
@@ -10,16 +10,19 @@ type Clip struct {
BaseModel BaseModel
ID JSONID `json:"id"` ID JSONID `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Version string `json:"version"`
PlayerID string `json:"player_id"`
UUID string `json:"uuid"`
Thumbnail string `json:"thumbnail"`
PreviewURL string `json:"preview_url"`
Duration int `json:"duration"` Duration int `json:"duration"`
CoverURI string `json:"cover_uri"` TrackIDs []int `json:"track_ids"`
PreviewURI string `json:"preview_uri"` Artists []Artist `json:"artists"`
Available bool `json:"available"` Disclaimers []string `json:"disclaimers"`
Views int `json:"views"` Explicit bool `json:"explicit"`
Likes int `json:"likes"` Cover *Cover `json:"cover"`
CreatedAt string `json:"created_at"` ContentRestrictions *ContentRestrictions `json:"content_restrictions"`
Artist *Artist `json:"artist"` Client *ClientInterface `json:"-"`
Track *Track `json:"track"`
Playlist *Playlist `json:"playlist"`
ClipType string `json:"clip_type"` ClipType string `json:"clip_type"`
IsVertical bool `json:"is_vertical"` IsVertical bool `json:"is_vertical"`
AspectRatio string `json:"aspect_ratio"` AspectRatio string `json:"aspect_ratio"`
@@ -40,26 +43,47 @@ func (c *Clip) DeJSON(data []byte, client ClientInterface) (Model, error) {
if v, ok := raw["title"]; ok { if v, ok := raw["title"]; ok {
json.Unmarshal(v, &clip.Title) json.Unmarshal(v, &clip.Title)
} }
if v, ok := raw["version"]; ok {
json.Unmarshal(v, &clip.Version)
}
if v, ok := raw["player_id"]; ok {
json.Unmarshal(v, &clip.PlayerID)
}
if v, ok := raw["uuid"]; ok {
json.Unmarshal(v, &clip.UUID)
}
if v, ok := raw["thumbnail"]; ok {
json.Unmarshal(v, &clip.Thumbnail)
}
if v, ok := raw["preview_url"]; ok {
json.Unmarshal(v, &clip.PreviewURL)
}
if v, ok := raw["duration"]; ok { if v, ok := raw["duration"]; ok {
json.Unmarshal(v, &clip.Duration) json.Unmarshal(v, &clip.Duration)
} }
if v, ok := raw["cover_uri"]; ok { if v, ok := raw["track_ids"]; ok {
json.Unmarshal(v, &clip.CoverURI) json.Unmarshal(v, &clip.TrackIDs)
} }
if v, ok := raw["preview_uri"]; ok { if v, ok := raw["artists"]; ok {
json.Unmarshal(v, &clip.PreviewURI) var artists []Artist
json.Unmarshal(v, &artists)
clip.Artists = artists
} }
if v, ok := raw["available"]; ok { if v, ok := raw["disclaimers"]; ok {
json.Unmarshal(v, &clip.Available) json.Unmarshal(v, &clip.Disclaimers)
} }
if v, ok := raw["views"]; ok { if v, ok := raw["explicit"]; ok {
json.Unmarshal(v, &clip.Views) json.Unmarshal(v, &clip.Explicit)
} }
if v, ok := raw["likes"]; ok { if v, ok := raw["cover"]; ok {
json.Unmarshal(v, &clip.Likes) var cover Cover
json.Unmarshal(v, &cover)
clip.Cover = &cover
} }
if v, ok := raw["created_at"]; ok { if v, ok := raw["content_restrictions"]; ok {
json.Unmarshal(v, &clip.CreatedAt) var cr ContentRestrictions
json.Unmarshal(v, &cr)
clip.ContentRestrictions = &cr
} }
if v, ok := raw["clip_type"]; ok { if v, ok := raw["clip_type"]; ok {
json.Unmarshal(v, &clip.ClipType) json.Unmarshal(v, &clip.ClipType)
@@ -71,22 +95,6 @@ func (c *Clip) DeJSON(data []byte, client ClientInterface) (Model, error) {
json.Unmarshal(v, &clip.AspectRatio) 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 return clip, nil
} }
+78
View File
@@ -13,6 +13,7 @@ type Concert struct {
Description string `json:"description"` Description string `json:"description"`
Date string `json:"date"` Date string `json:"date"`
Time string `json:"time"` Time string `json:"time"`
Datetime string `json:"datetime"`
Venue *ConcertVenue `json:"venue"` Venue *ConcertVenue `json:"venue"`
Location *ConcertLocation `json:"location"` Location *ConcertLocation `json:"location"`
MinPrice int `json:"min_price"` MinPrice int `json:"min_price"`
@@ -27,6 +28,13 @@ type Concert struct {
Cashback *ConcertCashback `json:"cashback"` Cashback *ConcertCashback `json:"cashback"`
Tabs []ConcertTabConfig `json:"tabs"` Tabs []ConcertTabConfig `json:"tabs"`
Skeleton *ConcertSkeleton `json:"skeleton"` Skeleton *ConcertSkeleton `json:"skeleton"`
AfishaURL string `json:"afisha_url"`
City string `json:"city"`
Place string `json:"place"`
Address string `json:"address"`
ContentRating string `json:"content_rating"`
DataSessionID string `json:"data_session_id"`
EventInfo *ConcertEventInfo `json:"event_info"`
} }
// DeJSON deserializes a Concert from JSON data. // DeJSON deserializes a Concert from JSON data.
@@ -77,6 +85,30 @@ func (c *Concert) DeJSON(data []byte, client ClientInterface) (Model, error) {
if v, ok := raw["genres"]; ok { if v, ok := raw["genres"]; ok {
json.Unmarshal(v, &concert.Genres) json.Unmarshal(v, &concert.Genres)
} }
if v, ok := raw["datetime"]; ok {
json.Unmarshal(v, &concert.Datetime)
}
if v, ok := raw["afisha_url"]; ok {
json.Unmarshal(v, &concert.AfishaURL)
}
if v, ok := raw["city"]; ok {
json.Unmarshal(v, &concert.City)
}
if v, ok := raw["place"]; ok {
json.Unmarshal(v, &concert.Place)
}
if v, ok := raw["address"]; ok {
json.Unmarshal(v, &concert.Address)
}
if v, ok := raw["content_rating"]; ok {
json.Unmarshal(v, &concert.ContentRating)
}
if v, ok := raw["data_session_id"]; ok {
json.Unmarshal(v, &concert.DataSessionID)
}
if v, ok := raw["event_info"]; ok {
json.Unmarshal(v, &concert.EventInfo)
}
if v, ok := raw["venue"]; ok { if v, ok := raw["venue"]; ok {
var venue ConcertVenue var venue ConcertVenue
@@ -242,3 +274,49 @@ type ConcertSkeletonBlock struct {
Title string `json:"title"` Title string `json:"title"`
Data json.RawMessage `json:"data"` Data json.RawMessage `json:"data"`
} }
// ArtistConcerts represents concerts by an artist.
type ArtistConcerts struct {
BaseModel
ArtistTitle string `json:"artist_title"`
Concerts []Concert `json:"concerts"`
}
// DeJSON deserializes ArtistConcerts from JSON data.
func (ac *ArtistConcerts) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
acObj := &ArtistConcerts{BaseModel: *NewBaseModel(c)}
if v, ok := raw["artist_title"]; ok {
json.Unmarshal(v, &acObj.ArtistTitle)
}
if v, ok := raw["concerts"]; ok {
var concerts []Concert
json.Unmarshal(v, &concerts)
acObj.Concerts = concerts
}
return acObj, nil
}
// DeList deserializes a list of ArtistConcerts from JSON data.
func (ac *ArtistConcerts) 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 {
ac := &ArtistConcerts{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, ac); err != nil {
continue
}
result = append(result, ac)
}
return result, nil
}
+82
View File
@@ -170,3 +170,85 @@ func (ltp *LicenceTextPart) DeList(data []byte, client ClientInterface) ([]Model
} }
return result, nil return result, nil
} }
// Disclaimer represents a disclaimer.
type Disclaimer struct {
BaseModel
ForeignAgent *ForeignAgent `json:"foreign_agent"`
}
// DeJSON deserializes Disclaimer from JSON data.
func (d *Disclaimer) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
disclaimer := &Disclaimer{BaseModel: *NewBaseModel(c)}
if v, ok := raw["foreign_agent"]; ok {
var fa ForeignAgent
json.Unmarshal(v, &fa)
disclaimer.ForeignAgent = &fa
}
return disclaimer, nil
}
// DeList deserializes a list of Disclaimers from JSON data.
func (d *Disclaimer) 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 {
disclaimer := &Disclaimer{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, disclaimer); err != nil {
continue
}
result = append(result, disclaimer)
}
return result, nil
}
// ForeignAgent represents a foreign agent.
type ForeignAgent struct {
BaseModel
Text string `json:"text"`
}
// DeJSON deserializes ForeignAgent from JSON data.
func (fa *ForeignAgent) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
agent := &ForeignAgent{BaseModel: *NewBaseModel(c)}
if v, ok := raw["text"]; ok {
json.Unmarshal(v, &agent.Text)
}
return agent, nil
}
// DeList deserializes a list of ForeignAgent from JSON data.
func (fa *ForeignAgent) 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 {
agent := &ForeignAgent{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, agent); err != nil {
continue
}
result = append(result, agent)
}
return result, nil
}
+24 -7
View File
@@ -8,9 +8,15 @@ import (
// Feed represents the main feed from the Yandex Music API. // Feed represents the main feed from the Yandex Music API.
type Feed struct { type Feed struct {
BaseModel BaseModel
CanGetMoreEvents bool `json:"can_get_more_events"`
Pumpkin bool `json:"pumpkin"`
IsWizardPassed bool `json:"is_wizard_passed"`
Headlines []string `json:"headlines"`
Today string `json:"today"`
Days []Day `json:"days"` Days []Day `json:"days"`
GeneratedPlaylists []GeneratedPlaylist `json:"generated_playlists"` GeneratedPlaylists []GeneratedPlaylist `json:"generated_playlists"`
PlayContexts []PlayContext `json:"play_contexts"` PlayContexts []PlayContext `json:"play_contexts"`
NextRevision string `json:"next_revision"`
} }
// DeJSON deserializes a Feed from JSON data. // DeJSON deserializes a Feed from JSON data.
@@ -22,6 +28,21 @@ func (f *Feed) DeJSON(data []byte, c ClientInterface) (Model, error) {
feed := &Feed{BaseModel: *NewBaseModel(c)} feed := &Feed{BaseModel: *NewBaseModel(c)}
if v, ok := raw["can_get_more_events"]; ok {
json.Unmarshal(v, &feed.CanGetMoreEvents)
}
if v, ok := raw["pumpkin"]; ok {
json.Unmarshal(v, &feed.Pumpkin)
}
if v, ok := raw["is_wizard_passed"]; ok {
json.Unmarshal(v, &feed.IsWizardPassed)
}
if v, ok := raw["headlines"]; ok {
json.Unmarshal(v, &feed.Headlines)
}
if v, ok := raw["today"]; ok {
json.Unmarshal(v, &feed.Today)
}
if v, ok := raw["days"]; ok { if v, ok := raw["days"]; ok {
var days []Day var days []Day
json.Unmarshal(v, &days) json.Unmarshal(v, &days)
@@ -37,6 +58,9 @@ func (f *Feed) DeJSON(data []byte, c ClientInterface) (Model, error) {
json.Unmarshal(v, &contexts) json.Unmarshal(v, &contexts)
feed.PlayContexts = contexts feed.PlayContexts = contexts
} }
if v, ok := raw["next_revision"]; ok {
json.Unmarshal(v, &feed.NextRevision)
}
return feed, nil return feed, nil
} }
@@ -270,10 +294,3 @@ type Ad struct {
Duration int `json:"duration"` 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"`
}
+16 -4
View File
@@ -12,8 +12,11 @@ type Label struct {
Name string `json:"name"` Name string `json:"name"`
CoverURI string `json:"cover_uri"` CoverURI string `json:"cover_uri"`
Description string `json:"description"` Description string `json:"description"`
DescriptionFormatted string `json:"description_formatted"`
Image string `json:"image"`
Website string `json:"website"` Website string `json:"website"`
SocialLinks []SocialLink `json:"social_links"` Links []Link `json:"links"`
Type string `json:"type"`
Available bool `json:"available"` Available bool `json:"available"`
Owner *User `json:"owner"` Owner *User `json:"owner"`
} }
@@ -45,10 +48,19 @@ func (l *Label) DeJSON(data []byte, client ClientInterface) (Model, error) {
if v, ok := raw["available"]; ok { if v, ok := raw["available"]; ok {
json.Unmarshal(v, &label.Available) json.Unmarshal(v, &label.Available)
} }
if v, ok := raw["social_links"]; ok { if v, ok := raw["description_formatted"]; ok {
var links []SocialLink json.Unmarshal(v, &label.DescriptionFormatted)
}
if v, ok := raw["image"]; ok {
json.Unmarshal(v, &label.Image)
}
if v, ok := raw["links"]; ok {
var links []Link
json.Unmarshal(v, &links) json.Unmarshal(v, &links)
label.SocialLinks = links label.Links = links
}
if v, ok := raw["type"]; ok {
json.Unmarshal(v, &label.Type)
} }
if v, ok := raw["owner"]; ok { if v, ok := raw["owner"]; ok {
var user User var user User
+59
View File
@@ -15,6 +15,8 @@ type Landing struct {
Type string `json:"type"` Type string `json:"type"`
Blocks []LandingBlock `json:"blocks"` Blocks []LandingBlock `json:"blocks"`
Available bool `json:"available"` Available bool `json:"available"`
Pumpkin bool `json:"pumpkin"`
ContentID string `json:"content_id"`
} }
// DeJSON deserializes a Landing from JSON data. // DeJSON deserializes a Landing from JSON data.
@@ -44,6 +46,12 @@ func (l *Landing) DeJSON(data []byte, c ClientInterface) (Model, error) {
if v, ok := raw["available"]; ok { if v, ok := raw["available"]; ok {
json.Unmarshal(v, &landing.Available) json.Unmarshal(v, &landing.Available)
} }
if v, ok := raw["pumpkin"]; ok {
json.Unmarshal(v, &landing.Pumpkin)
}
if v, ok := raw["content_id"]; ok {
json.Unmarshal(v, &landing.ContentID)
}
if v, ok := raw["blocks"]; ok { if v, ok := raw["blocks"]; ok {
var blocks []LandingBlock var blocks []LandingBlock
@@ -94,12 +102,63 @@ type LandingBlockData struct {
// LandingList represents a list of landings. // LandingList represents a list of landings.
type LandingList struct { type LandingList struct {
BaseModel BaseModel
Type string `json:"type"`
TypeForFrom string `json:"type_for_from"`
Title string `json:"title"`
ID string `json:"id"`
Landings []Landing `json:"landings"` Landings []Landing `json:"landings"`
NewReleases []int `json:"new_releases"`
NewPlaylists []PlaylistID `json:"new_playlists"`
Podcasts []int `json:"podcasts"`
Total int `json:"total"` Total int `json:"total"`
Page int `json:"page"` Page int `json:"page"`
PageSize int `json:"page_size"` PageSize int `json:"page_size"`
} }
// PlaylistID represents a playlist ID reference.
type PlaylistID struct {
BaseModel
UID int `json:"uid"`
Kind int `json:"kind"`
}
// DeJSON deserializes a PlaylistID from JSON data.
func (pid *PlaylistID) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
playlistID := &PlaylistID{BaseModel: *NewBaseModel(c)}
if v, ok := raw["uid"]; ok {
json.Unmarshal(v, &playlistID.UID)
}
if v, ok := raw["kind"]; ok {
json.Unmarshal(v, &playlistID.Kind)
}
return playlistID, nil
}
// DeList deserializes a list of PlaylistID from JSON data.
func (pid *PlaylistID) 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 {
pid := &PlaylistID{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, pid); err != nil {
continue
}
result = append(result, pid)
}
return result, nil
}
// DeJSON deserializes a LandingList from JSON data. // DeJSON deserializes a LandingList from JSON data.
func (ll *LandingList) DeJSON(data []byte, c ClientInterface) (Model, error) { func (ll *LandingList) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage var raw map[string]json.RawMessage
+1 -1
View File
@@ -133,7 +133,7 @@ type MetatagSortByValue struct {
type MetatagArtistEntry struct { type MetatagArtistEntry struct {
BaseModel BaseModel
Artist *Artist `json:"artist"` Artist *Artist `json:"artist"`
Score float64 `json:"score"` PopularTracks []Track `json:"popular_tracks"`
} }
// Genre represents a genre from the Yandex Music API. // Genre represents a genre from the Yandex Music API.
-1
View File
@@ -110,7 +110,6 @@ type MusicHistoryItemData struct {
PlayContext *PlayContext `json:"play_context"` PlayContext *PlayContext `json:"play_context"`
Position int `json:"position"` Position int `json:"position"`
Duration int `json:"duration"` Duration int `json:"duration"`
Progress float64 `json:"progress"`
} }
// MusicHistoryItemID represents music history item ID. // MusicHistoryItemID represents music history item ID.
+45
View File
@@ -21,6 +21,7 @@ type Pin struct {
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at"` ExpiresAt string `json:"expires_at"`
Active bool `json:"active"` Active bool `json:"active"`
ContentRestrictions *ContentRestrictions `json:"content_restrictions"`
} }
// DeJSON deserializes a Pin from JSON data. // DeJSON deserializes a Pin from JSON data.
@@ -59,6 +60,11 @@ func (p *Pin) DeJSON(data []byte, client ClientInterface) (Model, error) {
if v, ok := raw["active"]; ok { if v, ok := raw["active"]; ok {
json.Unmarshal(v, &pin.Active) json.Unmarshal(v, &pin.Active)
} }
if v, ok := raw["content_restrictions"]; ok {
var cr ContentRestrictions
json.Unmarshal(v, &cr)
pin.ContentRestrictions = &cr
}
if v, ok := raw["track"]; ok { if v, ok := raw["track"]; ok {
var track Track var track Track
@@ -121,6 +127,8 @@ type PinsList struct {
// Presaves represents presaves. // Presaves represents presaves.
type Presaves struct { type Presaves struct {
BaseModel BaseModel
UpcomingAlbums []Album `json:"upcoming_albums"`
ReleasedAlbums []Album `json:"released_albums"`
Presaves []Presave `json:"presaves"` Presaves []Presave `json:"presaves"`
Total int `json:"total"` Total int `json:"total"`
Page int `json:"page"` Page int `json:"page"`
@@ -213,3 +221,40 @@ func (p *Presave) DeList(data []byte, client ClientInterface) ([]Model, error) {
} }
return result, nil return result, nil
} }
// DeJSON deserializes Presaves from JSON data.
func (p *Presaves) DeJSON(data []byte, client ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
presaves := &Presaves{BaseModel: *NewBaseModel(client)}
if v, ok := raw["upcoming_albums"]; ok {
var albums []Album
json.Unmarshal(v, &albums)
presaves.UpcomingAlbums = albums
}
if v, ok := raw["released_albums"]; ok {
var albums []Album
json.Unmarshal(v, &albums)
presaves.ReleasedAlbums = albums
}
if v, ok := raw["presaves"]; ok {
var presaveList []Presave
json.Unmarshal(v, &presaveList)
presaves.Presaves = presaveList
}
if v, ok := raw["total"]; ok {
json.Unmarshal(v, &presaves.Total)
}
if v, ok := raw["page"]; ok {
json.Unmarshal(v, &presaves.Page)
}
if v, ok := raw["page_size"]; ok {
json.Unmarshal(v, &presaves.PageSize)
}
return presaves, nil
}
+5 -3
View File
@@ -93,13 +93,15 @@ func (q *Queue) DeList(data []byte, c ClientInterface) ([]Model, error) {
// QueueItem represents a queue item. // QueueItem represents a queue item.
type QueueItem struct { type QueueItem struct {
BaseModel BaseModel
ID JSONID `json:"id"`
TrackID string `json:"track_id"` TrackID string `json:"track_id"`
Track *Track `json:"track"` Track *Track `json:"track"`
Position int `json:"position"` Position int `json:"position"`
AddedAt string `json:"added_at"` AddedAt string `json:"added_at"`
AddedBy string `json:"added_by"` AddedBy string `json:"added_by"`
Playable bool `json:"playable"` Playable bool `json:"playable"`
PlayContext *PlayContext `json:"play_context"` Modified string `json:"modified"`
Context *PlayContext `json:"context"`
} }
// DeJSON deserializes a QueueItem from JSON data. // DeJSON deserializes a QueueItem from JSON data.
@@ -132,10 +134,10 @@ func (qi *QueueItem) DeJSON(data []byte, c ClientInterface) (Model, error) {
json.Unmarshal(v, &track) json.Unmarshal(v, &track)
item.Track = &track item.Track = &track
} }
if v, ok := raw["play_context"]; ok { if v, ok := raw["context"]; ok {
var ctx PlayContext var ctx PlayContext
json.Unmarshal(v, &ctx) json.Unmarshal(v, &ctx)
item.PlayContext = &ctx item.Context = &ctx
} }
return item, nil return item, nil
+393 -2
View File
@@ -192,8 +192,11 @@ func (r *RotorStation) DeList(data []byte, client ClientInterface) ([]Model, err
// RotorSettings represents rotor station settings. // RotorSettings represents rotor station settings.
type RotorSettings struct { type RotorSettings struct {
BaseModel BaseModel
Diversity float64 `json:"diversity"` Language string `json:"language"`
Energy float64 `json:"energy"` Diversity string `json:"diversity"`
Mood int `json:"mood"`
Energy int `json:"energy"`
MoodEnergy string `json:"mood_energy"`
Acousticness float64 `json:"acousticness"` Acousticness float64 `json:"acousticness"`
Instrumentalness float64 `json:"instrumentalness"` Instrumentalness float64 `json:"instrumentalness"`
Live float64 `json:"live"` Live float64 `json:"live"`
@@ -314,6 +317,70 @@ func (r *RotorSession) DeList(data []byte, client ClientInterface) ([]Model, err
return result, nil return result, nil
} }
// AdParams represents ad parameters.
type AdParams struct {
BaseModel
PartnerID string `json:"partner_id"`
CategoryID string `json:"category_id"`
PageRef string `json:"page_ref"`
TargetRef string `json:"target_ref"`
OtherParams string `json:"other_params"`
GenreID string `json:"genre_id"`
GenreName string `json:"genre_name"`
}
// DeJSON deserializes AdParams from JSON data.
func (ap *AdParams) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
params := &AdParams{BaseModel: *NewBaseModel(c)}
if v, ok := raw["partner_id"]; ok {
json.Unmarshal(v, &params.PartnerID)
}
if v, ok := raw["category_id"]; ok {
json.Unmarshal(v, &params.CategoryID)
}
if v, ok := raw["page_ref"]; ok {
json.Unmarshal(v, &params.PageRef)
}
if v, ok := raw["target_ref"]; ok {
json.Unmarshal(v, &params.TargetRef)
}
if v, ok := raw["other_params"]; ok {
json.Unmarshal(v, &params.OtherParams)
}
if v, ok := raw["genre_id"]; ok {
json.Unmarshal(v, &params.GenreID)
}
if v, ok := raw["genre_name"]; ok {
json.Unmarshal(v, &params.GenreName)
}
return params, nil
}
// DeList deserializes a list of AdParams from JSON data.
func (ap *AdParams) 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 {
params := &AdParams{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, params); err != nil {
continue
}
result = append(result, params)
}
return result, nil
}
// SessionFeedback represents session feedback. // SessionFeedback represents session feedback.
type SessionFeedback struct { type SessionFeedback struct {
BaseModel BaseModel
@@ -327,3 +394,327 @@ type SessionFeedbacks struct {
BaseModel BaseModel
Feedbacks []SessionFeedback `json:"feedbacks"` Feedbacks []SessionFeedback `json:"feedbacks"`
} }
// Dashboard represents rotor stations dashboard.
type Dashboard struct {
BaseModel
DashboardID string `json:"dashboard_id"`
Stations []StationResult `json:"stations"`
Pumpkin bool `json:"pumpkin"`
}
// DeJSON deserializes Dashboard from JSON data.
func (d *Dashboard) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
dashboard := &Dashboard{BaseModel: *NewBaseModel(c)}
if v, ok := raw["dashboard_id"]; ok {
json.Unmarshal(v, &dashboard.DashboardID)
}
if v, ok := raw["stations"]; ok {
var stations []StationResult
json.Unmarshal(v, &stations)
dashboard.Stations = stations
}
if v, ok := raw["pumpkin"]; ok {
json.Unmarshal(v, &dashboard.Pumpkin)
}
return dashboard, nil
}
// DeList deserializes a list of Dashboards from JSON data.
func (d *Dashboard) 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 {
dashboard := &Dashboard{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, dashboard); err != nil {
continue
}
result = append(result, dashboard)
}
return result, nil
}
// StationResult represents a station result.
type StationResult struct {
BaseModel
Station *Station `json:"station"`
Settings *RotorSettings `json:"settings"`
Settings2 *RotorSettings `json:"settings2"`
AdParams *AdParams `json:"ad_params"`
}
// DeJSON deserializes StationResult from JSON data.
func (sr *StationResult) 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 := &StationResult{BaseModel: *NewBaseModel(c)}
if v, ok := raw["station"]; ok {
var station Station
json.Unmarshal(v, &station)
result.Station = &station
}
if v, ok := raw["settings"]; ok {
var settings RotorSettings
json.Unmarshal(v, &settings)
result.Settings = &settings
}
if v, ok := raw["settings2"]; ok {
var settings2 RotorSettings
json.Unmarshal(v, &settings2)
result.Settings2 = &settings2
}
if v, ok := raw["ad_params"]; ok {
var adParams AdParams
json.Unmarshal(v, &adParams)
result.AdParams = &adParams
}
return result, nil
}
// DeList deserializes a list of StationResults from JSON data.
func (sr *StationResult) 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 {
sr := &StationResult{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, sr); err != nil {
continue
}
result = append(result, sr)
}
return result, nil
}
// GenerativeStream represents a generative stream.
type GenerativeStream struct {
BaseModel
Data *GenerativeStreamData `json:"data"`
Version string `json:"version"`
Stream *GenerativeStreamInfo `json:"stream"`
}
// DeJSON deserializes GenerativeStream from JSON data.
func (gs *GenerativeStream) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
stream := &GenerativeStream{BaseModel: *NewBaseModel(c)}
if v, ok := raw["data"]; ok {
var data GenerativeStreamData
json.Unmarshal(v, &data)
stream.Data = &data
}
if v, ok := raw["version"]; ok {
json.Unmarshal(v, &stream.Version)
}
if v, ok := raw["stream"]; ok {
var streamInfo GenerativeStreamInfo
json.Unmarshal(v, &streamInfo)
stream.Stream = &streamInfo
}
return stream, nil
}
// DeList deserializes a list of GenerativeStreams from JSON data.
func (gs *GenerativeStream) 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 {
stream := &GenerativeStream{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, stream); err != nil {
continue
}
result = append(result, stream)
}
return result, nil
}
// Station represents a radio station (used in StationResult).
type Station struct {
BaseModel
ID JSONID `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
IDForFrom string `json:"id_for_from"`
}
// DeJSON deserializes Station from JSON data.
func (s *Station) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
station := &Station{BaseModel: *NewBaseModel(c)}
if v, ok := raw["id"]; ok {
json.Unmarshal(v, &station.ID)
}
if v, ok := raw["name"]; ok {
json.Unmarshal(v, &station.Name)
}
if v, ok := raw["icon"]; ok {
json.Unmarshal(v, &station.Icon)
}
if v, ok := raw["id_for_from"]; ok {
json.Unmarshal(v, &station.IDForFrom)
}
return station, nil
}
// DeList deserializes a list of Stations from JSON data.
func (s *Station) 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 {
station := &Station{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, station); err != nil {
continue
}
result = append(result, station)
}
return result, nil
}
// GenerativeStreamData represents generative stream data.
type GenerativeStreamData struct {
BaseModel
Title string `json:"title"`
Subtitle string `json:"subtitle"`
BackgroundColor string `json:"background_color"`
DerivedColors *CoverDerivedColors `json:"derived_colors"`
ImageURL string `json:"image_url"`
VideoCoverURI string `json:"video_cover_uri"`
Explanations string `json:"explanations"`
}
// DeJSON deserializes GenerativeStreamData from JSON data.
func (gsd *GenerativeStreamData) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
gsdData := &GenerativeStreamData{BaseModel: *NewBaseModel(c)}
if v, ok := raw["title"]; ok {
json.Unmarshal(v, &gsdData.Title)
}
if v, ok := raw["subtitle"]; ok {
json.Unmarshal(v, &gsdData.Subtitle)
}
if v, ok := raw["background_color"]; ok {
json.Unmarshal(v, &gsdData.BackgroundColor)
}
if v, ok := raw["derived_colors"]; ok {
var dc CoverDerivedColors
json.Unmarshal(v, &dc)
gsdData.DerivedColors = &dc
}
if v, ok := raw["image_url"]; ok {
json.Unmarshal(v, &gsdData.ImageURL)
}
if v, ok := raw["video_cover_uri"]; ok {
json.Unmarshal(v, &gsdData.VideoCoverURI)
}
if v, ok := raw["explanations"]; ok {
json.Unmarshal(v, &gsdData.Explanations)
}
return gsdData, nil
}
// DeList deserializes a list of GenerativeStreamData from JSON data.
func (gsd *GenerativeStreamData) 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 {
data := &GenerativeStreamData{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, data); err != nil {
continue
}
result = append(result, data)
}
return result, nil
}
// GenerativeStreamInfo represents generative stream info.
type GenerativeStreamInfo struct {
BaseModel
ID string `json:"id"`
URL string `json:"url"`
}
// DeJSON deserializes GenerativeStreamInfo from JSON data.
func (gsi *GenerativeStreamInfo) 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 := &GenerativeStreamInfo{BaseModel: *NewBaseModel(c)}
if v, ok := raw["id"]; ok {
json.Unmarshal(v, &info.ID)
}
if v, ok := raw["url"]; ok {
json.Unmarshal(v, &info.URL)
}
return info, nil
}
// DeList deserializes a list of GenerativeStreamInfo from JSON data.
func (gsi *GenerativeStreamInfo) 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 := &GenerativeStreamInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
}
+13
View File
@@ -3,12 +3,25 @@ package models
// SearchResult represents search results from the Yandex Music API. // SearchResult represents search results from the Yandex Music API.
type SearchResult struct { type SearchResult struct {
SearchRequestID string `json:"search_request_id"`
Text string `json:"text"`
Best *Best `json:"best"`
Tracks *SearchTracksResult `json:"tracks"` Tracks *SearchTracksResult `json:"tracks"`
Albums *SearchAlbumsResult `json:"albums"` Albums *SearchAlbumsResult `json:"albums"`
Artists *SearchArtistsResult `json:"artists"` Artists *SearchArtistsResult `json:"artists"`
Playlists *SearchPlaylistsResult `json:"playlists"` Playlists *SearchPlaylistsResult `json:"playlists"`
Videos *SearchVideosResult `json:"videos"` Videos *SearchVideosResult `json:"videos"`
Users *SearchUsersResult `json:"users"` Users *SearchUsersResult `json:"users"`
Podcasts *SearchAlbumsResult `json:"podcasts"`
PodcastEpisodes *SearchTracksResult `json:"podcast_episodes"`
Clips *SearchVideosResult `json:"clips"`
Banner *SearchBanner `json:"banner"`
Page int `json:"page"`
PerPage int `json:"per_page"`
MisspellResult bool `json:"misspell_result"`
MisspellOriginal string `json:"misspell_original"`
MisspellCorrected bool `json:"misspell_corrected"`
Nocorrect bool `json:"nocorrect"`
} }
// SearchTracksResult represents track search results. // SearchTracksResult represents track search results.
+504 -23
View File
@@ -10,21 +10,65 @@ type Track struct {
BaseModel BaseModel
ID JSONID `json:"id"` ID JSONID `json:"id"`
Title string `json:"title"` Title string `json:"title"`
DurationMs int `json:"duration_ms"` Available bool `json:"available"`
StorageID string `json:"storage_id"`
CoverURI string `json:"cover_uri"`
Artists []Artist `json:"artists"` Artists []Artist `json:"artists"`
Albums []Album `json:"albums"` 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"` AvailableForPremiumUsers bool `json:"available_for_premium_users"`
R128 JSONR128 `json:"r128"` LyricsAvailable bool `json:"lyrics_available"`
TrackSharingFlag string `json:"track_sharing_flag"` Best bool `json:"best"`
RealID JSONID `json:"real_id"`
OgImage string `json:"og_image"`
Type string `json:"type"`
CoverURI string `json:"cover_uri"`
Major *Major `json:"major"` Major *Major `json:"major"`
Lyrics *TrackLyricsInfo `json:"lyrics"` DurationMs int `json:"duration_ms"`
StorageDir string `json:"storage_dir"`
FileSize int64 `json:"file_size"`
Genre string `json:"genre"`
AvailableAsRingtone bool `json:"available_as_ringtone"`
RingtoneID string `json:"ringtone_id"`
SimilarTracks []Track `json:"similar_tracks"` SimilarTracks []Track `json:"similar_tracks"`
Position *TrackPosition `json:"position"`
Substituted *Track `json:"substituted"`
MatchedTrack *Track `json:"matched_track"`
Normalization *Normalization `json:"normalization"`
Error string `json:"error"`
CanPublish bool `json:"can_publish"`
State string `json:"state"`
DesiredVisibility string `json:"desired_visibility"`
Filename string `json:"filename"`
UserInfo *User `json:"user_info"`
MetaData *MetaData `json:"meta_data"`
Regions []string `json:"regions"`
AvailableAsRbt bool `json:"available_as_rbt"`
ContentWarning string `json:"content_warning"`
Explicit bool `json:"explicit"`
PreviewDurationMs int `json:"preview_duration_ms"`
AvailableFullWithoutPermission bool `json:"available_full_without_permission"`
Version string `json:"version"`
RememberPosition bool `json:"remember_position"`
BackgroundVideoURI string `json:"background_video_uri"`
ShortDescription string `json:"short_description"`
IsSuitableForChildren bool `json:"is_suitable_for_children"`
TrackSource string `json:"track_source"`
AvailableForOptions []string `json:"available_for_options"`
R128 *R128 `json:"r128"`
LyricsInfo *TrackLyricsInfo `json:"lyrics_info"`
TrackSharingFlag string `json:"track_sharing_flag"`
DerivedColors *CoverDerivedColors `json:"derived_colors"`
Fade *Fade `json:"fade"`
SmartPreviewParams *SmartPreviewParams `json:"smart_preview_params"`
SpecialAudioResources []string `json:"special_audio_resources"`
Disclaimers []string `json:"disclaimers"`
BackgroundVideoID string `json:"background_video_id"`
PlayerID string `json:"player_id"`
MixFade *Fade `json:"mix_fade"`
Chart *Chart `json:"chart"`
ClipIDs []int `json:"clip_ids"`
Isrc string `json:"isrc"`
UgcArtistName string `json:"ugc_artist_name"`
PodcastEpisodeType string `json:"podcast_episode_type"`
PubDate string `json:"pub_date"`
DownloadInfo []DownloadInfo `json:"download_info"` DownloadInfo []DownloadInfo `json:"download_info"`
} }
@@ -47,8 +91,8 @@ func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) {
if v, ok := raw["duration_ms"]; ok { if v, ok := raw["duration_ms"]; ok {
json.Unmarshal(v, &track.DurationMs) json.Unmarshal(v, &track.DurationMs)
} }
if v, ok := raw["storage_id"]; ok { if v, ok := raw["storage_dir"]; ok {
json.Unmarshal(v, &track.StorageID) json.Unmarshal(v, &track.StorageDir)
} }
if v, ok := raw["cover_uri"]; ok { if v, ok := raw["cover_uri"]; ok {
json.Unmarshal(v, &track.CoverURI) json.Unmarshal(v, &track.CoverURI)
@@ -74,6 +118,12 @@ func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) {
if v, ok := raw["track_sharing_flag"]; ok { if v, ok := raw["track_sharing_flag"]; ok {
json.Unmarshal(v, &track.TrackSharingFlag) 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 // Nested objects
if v, ok := raw["artists"]; ok { if v, ok := raw["artists"]; ok {
@@ -94,10 +144,10 @@ func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) {
track.Major = &major track.Major = &major
} }
} }
if v, ok := raw["lyrics"]; ok { if v, ok := raw["lyrics_info"]; ok {
var lyrics TrackLyricsInfo var lyrics TrackLyricsInfo
if err := json.Unmarshal(v, &lyrics); err == nil { if err := json.Unmarshal(v, &lyrics); err == nil {
track.Lyrics = &lyrics track.LyricsInfo = &lyrics
} }
} }
if v, ok := raw["similar_tracks"]; ok { if v, ok := raw["similar_tracks"]; ok {
@@ -112,6 +162,12 @@ func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) {
track.DownloadInfo = downloadInfo 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 return track, nil
} }
@@ -144,9 +200,6 @@ func (t *Track) GetDownloadInfo(quality string) *DownloadInfo {
return nil 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 label info for a track. // Major represents major label info for a track.
type Major struct { type Major struct {
ID JSONID `json:"id"` ID JSONID `json:"id"`
@@ -154,13 +207,48 @@ type Major struct {
Description string `json:"description"` Description string `json:"description"`
} }
// TrackLyricsInfo represents minimal lyrics info for a track. // TrackLyricsInfo represents lyrics availability info for a track.
type TrackLyricsInfo struct { type TrackLyricsInfo struct {
ID JSONID `json:"id"` BaseModel
Text string `json:"text"` HasAvailableSyncLyrics bool `json:"has_available_sync_lyrics"`
Major *Major `json:"major"` HasAvailableTextLyrics bool `json:"has_available_text_lyrics"`
Available bool `json:"available"` }
HasText bool `json:"has_text"`
// DeJSON deserializes TrackLyricsInfo from JSON data.
func (tli *TrackLyricsInfo) 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 := &TrackLyricsInfo{BaseModel: *NewBaseModel(c)}
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)
}
return info, nil
}
// DeList deserializes a list of TrackLyricsInfo from JSON data.
func (tli *TrackLyricsInfo) 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 := &TrackLyricsInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
} }
// DownloadInfo represents file download info for a track. // DownloadInfo represents file download info for a track.
@@ -171,4 +259,397 @@ type DownloadInfo struct {
Size int64 `json:"size"` Size int64 `json:"size"`
URL string `json:"url"` URL string `json:"url"`
DirectLink bool `json:"direct_link"` DirectLink bool `json:"direct_link"`
DownloadInfoURL string `json:"download_info_url"`
DirectLinkURL string `json:"-"` // Computed signed download URL
}
// Normalization represents track normalization values.
type Normalization struct {
BaseModel
Gain float64 `json:"gain"`
Peak int `json:"peak"`
}
// MetaData represents track metadata.
type MetaData struct {
BaseModel
Album string `json:"album"`
Volume int `json:"volume"`
Year int `json:"year"`
Number int `json:"number"`
Genre string `json:"genre"`
Lyricist string `json:"lyricist"`
Version string `json:"version"`
Composer string `json:"composer"`
}
// Fade represents fade parameters for a track.
type Fade struct {
BaseModel
InStart float64 `json:"in_start"`
InStop float64 `json:"in_stop"`
OutStart float64 `json:"out_start"`
OutStop float64 `json:"out_stop"`
}
// SmartPreviewParams represents smart preview parameters.
type SmartPreviewParams struct {
BaseModel
DurationMs int `json:"duration_ms"`
Fade *Fade `json:"fade"`
}
// CoverDerivedColors represents derived colors from cover art.
type CoverDerivedColors struct {
BaseModel
Average string `json:"average"`
WaveText string `json:"wave_text"`
MiniPlayer string `json:"mini_player"`
Accent string `json:"accent"`
}
// CustomWave represents a custom wave for playlists.
type CustomWave struct {
BaseModel
Title string `json:"title"`
AnimationURL string `json:"animation_url"`
Position string `json:"position"`
Header string `json:"header"`
BackgroundImageURL string `json:"background_image_url"`
SquareAgentAnimation string `json:"square_agent_animation"`
}
// TrackId represents a track ID reference.
type TrackId struct {
BaseModel
ID JSONID `json:"id"`
TrackID JSONID `json:"track_id"`
AlbumID JSONID `json:"album_id"`
From string `json:"from"`
}
// DeJSON deserializes a TrackId from JSON data.
func (ti *TrackId) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
tid := &TrackId{BaseModel: *NewBaseModel(c)}
if v, ok := raw["id"]; ok {
json.Unmarshal(v, &tid.ID)
}
if v, ok := raw["track_id"]; ok {
json.Unmarshal(v, &tid.TrackID)
}
if v, ok := raw["album_id"]; ok {
json.Unmarshal(v, &tid.AlbumID)
}
if v, ok := raw["from"]; ok {
json.Unmarshal(v, &tid.From)
}
return tid, nil
}
// DeList deserializes a list of TrackId from JSON data.
func (ti *TrackId) 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 {
tid := &TrackId{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, tid); err != nil {
continue
}
result = append(result, tid)
}
return result, nil
}
// TrackFullInfo represents full track info.
type TrackFullInfo struct {
BaseModel
Track *Track `json:"track"`
SimilarTracks []Track `json:"similar_tracks"`
AlsoInAlbums []Track `json:"also_in_albums"`
Aliases []string `json:"aliases"`
Artists []Artist `json:"artists"`
}
// DeJSON deserializes a TrackFullInfo from JSON data.
func (tfi *TrackFullInfo) 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 := &TrackFullInfo{BaseModel: *NewBaseModel(c)}
if v, ok := raw["track"]; ok {
var track Track
if err := json.Unmarshal(v, &track); err == nil {
info.Track = &track
}
}
if v, ok := raw["similar_tracks"]; ok {
var tracks []Track
if err := json.Unmarshal(v, &tracks); err == nil {
info.SimilarTracks = tracks
}
}
if v, ok := raw["also_in_albums"]; ok {
var tracks []Track
if err := json.Unmarshal(v, &tracks); err == nil {
info.AlsoInAlbums = tracks
}
}
if v, ok := raw["aliases"]; ok {
json.Unmarshal(v, &info.Aliases)
}
if v, ok := raw["artists"]; ok {
var artists []Artist
if err := json.Unmarshal(v, &artists); err == nil {
info.Artists = artists
}
}
return info, nil
}
// DeList deserializes a list of TrackFullInfo from JSON data.
func (tfi *TrackFullInfo) 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 := &TrackFullInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
}
// SimilarTracks represents similar tracks.
type SimilarTracks struct {
BaseModel
Track *Track `json:"track"`
SimilarTracks []Track `json:"similar_tracks"`
}
// DeJSON deserializes SimilarTracks from JSON data.
func (st *SimilarTracks) DeJSON(data []byte, c ClientInterface) (Model, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
similar := &SimilarTracks{BaseModel: *NewBaseModel(c)}
if v, ok := raw["track"]; ok {
var track Track
if err := json.Unmarshal(v, &track); err == nil {
similar.Track = &track
}
}
if v, ok := raw["similar_tracks"]; ok {
var tracks []Track
if err := json.Unmarshal(v, &tracks); err == nil {
similar.SimilarTracks = tracks
}
}
return similar, nil
}
// DeList deserializes a list of SimilarTracks from JSON data.
func (st *SimilarTracks) 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 {
similar := &SimilarTracks{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, similar); err != nil {
continue
}
result = append(result, similar)
}
return result, nil
}
// TrackTrailer represents track trailer info.
type TrackTrailer struct {
BaseModel
Title string `json:"title"`
Track *Track `json:"track"`
}
// DeJSON deserializes a TrackTrailer from JSON data.
func (tt *TrackTrailer) 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 := &TrackTrailer{BaseModel: *NewBaseModel(c)}
if v, ok := raw["title"]; ok {
json.Unmarshal(v, &trailer.Title)
}
if v, ok := raw["track"]; ok {
var track Track
if err := json.Unmarshal(v, &track); err == nil {
trailer.Track = &track
}
}
return trailer, nil
}
// DeList deserializes a list of TrackTrailer from JSON data.
func (tt *TrackTrailer) 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 := &TrackTrailer{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, trailer); err != nil {
continue
}
result = append(result, trailer)
}
return result, nil
}
// FileInfo represents file info for a track.
type FileInfo struct {
BaseModel
DownloadInfo *FileDownloadInfo `json:"download_info"`
}
// DeJSON deserializes FileInfo from JSON data.
func (fi *FileInfo) 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 := &FileInfo{BaseModel: *NewBaseModel(c)}
if v, ok := raw["download_info"]; ok {
var di FileDownloadInfo
json.Unmarshal(v, &di)
info.DownloadInfo = &di
}
return info, nil
}
// DeList deserializes a list of FileInfo from JSON data.
func (fi *FileInfo) 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 := &FileInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
}
// FileDownloadInfo represents file download info.
type FileDownloadInfo struct {
BaseModel
TrackID string `json:"track_id"`
Quality string `json:"quality"`
Codec string `json:"codec"`
Bitrate int `json:"bitrate"`
Transport string `json:"transport"`
URL string `json:"url"`
URLs []string `json:"urls"`
RealID string `json:"real_id"`
Gain bool `json:"gain"`
Key string `json:"key"`
}
// DeJSON deserializes FileDownloadInfo from JSON data.
func (fdi *FileDownloadInfo) 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 := &FileDownloadInfo{BaseModel: *NewBaseModel(c)}
if v, ok := raw["track_id"]; ok {
json.Unmarshal(v, &info.TrackID)
}
if v, ok := raw["quality"]; ok {
json.Unmarshal(v, &info.Quality)
}
if v, ok := raw["codec"]; ok {
json.Unmarshal(v, &info.Codec)
}
if v, ok := raw["bitrate"]; ok {
json.Unmarshal(v, &info.Bitrate)
}
if v, ok := raw["transport"]; ok {
json.Unmarshal(v, &info.Transport)
}
if v, ok := raw["url"]; ok {
json.Unmarshal(v, &info.URL)
}
if v, ok := raw["urls"]; ok {
json.Unmarshal(v, &info.URLs)
}
if v, ok := raw["real_id"]; ok {
json.Unmarshal(v, &info.RealID)
}
if v, ok := raw["gain"]; ok {
json.Unmarshal(v, &info.Gain)
}
if v, ok := raw["key"]; ok {
json.Unmarshal(v, &info.Key)
}
return info, nil
}
// DeList deserializes a list of FileDownloadInfo from JSON data.
func (fdi *FileDownloadInfo) 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 := &FileDownloadInfo{BaseModel: *NewBaseModel(c)}
if err := json.Unmarshal(item, info); err != nil {
continue
}
result = append(result, info)
}
return result, nil
} }
+6 -6
View File
@@ -11,7 +11,7 @@ type TrackShort struct {
ID JSONID `json:"id"` ID JSONID `json:"id"`
Title string `json:"title"` Title string `json:"title"`
DurationMs int `json:"duration_ms"` DurationMs int `json:"duration_ms"`
StorageID string `json:"storage_id"` StorageDir string `json:"storage_dir"`
CoverURI string `json:"cover_uri"` CoverURI string `json:"cover_uri"`
Artists []Artist `json:"artists"` Artists []Artist `json:"artists"`
Albums []Album `json:"albums"` Albums []Album `json:"albums"`
@@ -20,10 +20,10 @@ type TrackShort struct {
Explicit bool `json:"explicit"` Explicit bool `json:"explicit"`
Available bool `json:"available"` Available bool `json:"available"`
AvailableForPremiumUsers bool `json:"available_for_premium_users"` AvailableForPremiumUsers bool `json:"available_for_premium_users"`
R128 JSONR128 `json:"r128"` R128 *R128 `json:"r128"`
TrackSharingFlag string `json:"track_sharing_flag"` TrackSharingFlag string `json:"track_sharing_flag"`
Major *Major `json:"major"` Major *Major `json:"major"`
Lyrics *SupplementLyrics `json:"lyrics"` Lyrics *TrackLyricsInfo `json:"lyrics"`
SimilarTracks []TrackShort `json:"similar_tracks"` SimilarTracks []TrackShort `json:"similar_tracks"`
DownloadInfo []DownloadInfo `json:"download_info"` DownloadInfo []DownloadInfo `json:"download_info"`
// Additional fields from track_short.py // Additional fields from track_short.py
@@ -51,8 +51,8 @@ func (t *TrackShort) DeJSON(data []byte, c ClientInterface) (Model, error) {
if v, ok := raw["duration_ms"]; ok { if v, ok := raw["duration_ms"]; ok {
json.Unmarshal(v, &track.DurationMs) json.Unmarshal(v, &track.DurationMs)
} }
if v, ok := raw["storage_id"]; ok { if v, ok := raw["storage_dir"]; ok {
json.Unmarshal(v, &track.StorageID) json.Unmarshal(v, &track.StorageDir)
} }
if v, ok := raw["cover_uri"]; ok { if v, ok := raw["cover_uri"]; ok {
json.Unmarshal(v, &track.CoverURI) json.Unmarshal(v, &track.CoverURI)
@@ -105,7 +105,7 @@ func (t *TrackShort) DeJSON(data []byte, c ClientInterface) (Model, error) {
} }
} }
if v, ok := raw["lyrics"]; ok { if v, ok := raw["lyrics"]; ok {
var lyrics SupplementLyrics var lyrics TrackLyricsInfo
if err := json.Unmarshal(v, &lyrics); err == nil { if err := json.Unmarshal(v, &lyrics); err == nil {
track.Lyrics = &lyrics track.Lyrics = &lyrics
} }
+4 -4
View File
@@ -43,7 +43,7 @@ func TestTrack_DeJSON(t *testing.T) {
"id": "123", "id": "123",
"title": "Test Track", "title": "Test Track",
"duration_ms": 180000, "duration_ms": 180000,
"storage_id": "456", "storage_dir": "456",
"cover_uri": "https://example.com/cover.jpg", "cover_uri": "https://example.com/cover.jpg",
"genre": "rock", "genre": "rock",
"version": "album", "version": "album",
@@ -55,7 +55,7 @@ func TestTrack_DeJSON(t *testing.T) {
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}], "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}], "albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
"major": {"id": "major1", "name": "Major Label", "description": "Big label"}, "major": {"id": "major1", "name": "Major Label", "description": "Big label"},
"lyrics": {"id": "lyr1", "text": "Test lyrics", "available": true, "has_text": true} "lyrics_info": {"has_available_sync_lyrics": true, "has_available_text_lyrics": true}
}`) }`)
track := &Track{BaseModel: *NewBaseModel(client)} track := &Track{BaseModel: *NewBaseModel(client)}
@@ -80,8 +80,8 @@ func TestTrack_DeJSON(t *testing.T) {
if resultTrack.Major == nil || resultTrack.Major.Name != "Major Label" { if resultTrack.Major == nil || resultTrack.Major.Name != "Major Label" {
t.Errorf("Expected Major 'Major Label'") t.Errorf("Expected Major 'Major Label'")
} }
if resultTrack.Lyrics == nil || resultTrack.Lyrics.Text != "Test lyrics" { if resultTrack.LyricsInfo == nil || resultTrack.LyricsInfo.HasAvailableSyncLyrics != true {
t.Errorf("Expected Lyrics 'Test lyrics'") t.Errorf("Expected LyricsInfo.HasAvailableSyncLyrics true")
} }
} }