diff --git a/internal/models/album.go b/internal/models/album.go index 73ce000..ec21f6a 100644 --- a/internal/models/album.go +++ b/internal/models/album.go @@ -8,35 +8,78 @@ import ( // Album represents a full album from the Yandex Music API. type Album struct { BaseModel - ID JSONID `json:"id"` - Title string `json:"title"` - CoverURI string `json:"cover_uri"` - Genre string `json:"genre"` - Year int `json:"year"` - TrackCount int `json:"track_count"` - Available bool `json:"available"` - ReleaseDate string `json:"release_date"` - Artists []Artist `json:"artists"` - Labels []LabelOrString `json:"labels"` - Volumes [][]Track `json:"volumes"` - Description string `json:"description"` - Meta *AlbumMeta `json:"meta"` - Best *Best `json:"best"` - // Additional fields - Region string `json:"region"` - AvailableForPremiumUsers bool `json:"available_for_premium_users"` - AvailableForPremiumUsersOnly bool `json:"available_for_premium_users_only"` - ContentWarning string `json:"content_warning"` - Label string `json:"label"` - CatalogNumber string `json:"catalog_number"` - Options []string `json:"options"` - TrackPositions []TrackPosition `json:"track_positions"` - SimilarAlbums []Album `json:"similar_albums"` - Trailer *AlbumTrailerStatus `json:"trailer"` + ID JSONID `json:"id"` + Error string `json:"error"` + Title string `json:"title"` + TrackCount int `json:"track_count"` + Artists []Artist `json:"artists"` + Labels []LabelOrString `json:"labels"` + 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"` + 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"` + Best *Best `json:"best"` + Region string `json:"region"` + AvailableForPremiumUsersOnly bool `json:"available_for_premium_users_only"` + Label string `json:"label"` + CatalogNumber string `json:"catalog_number"` + Options []string `json:"options"` + TrackPositions []TrackPosition `json:"track_positions"` + SimilarAlbums []Album `json:"similar_albums"` } // LabelOrString represents a label that can be either a Label object or a string. -// The Yandex Music API sometimes returns labels as objects and sometimes as strings. type LabelOrString struct { Label *Label `json:"-"` Str string `json:"-"` @@ -45,14 +88,12 @@ type LabelOrString struct { // UnmarshalJSON implements json.Unmarshaler, accepting both Label objects and strings. func (l *LabelOrString) UnmarshalJSON(data []byte) error { - // Try string first var s string if err := json.Unmarshal(data, &s); err == nil { l.Str = s l.IsStr = true return nil } - // Try Label object var label Label if err := json.Unmarshal(data, &label); err != nil { return err @@ -81,46 +122,6 @@ func (l LabelOrString) String() string { return "" } -// 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 -} - // DeJSON deserializes an Album from JSON data. func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) { var raw map[string]json.RawMessage @@ -134,6 +135,9 @@ func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) { if v, ok := raw["id"]; ok { json.Unmarshal(v, &album.ID) } + if v, ok := raw["error"]; ok { + json.Unmarshal(v, &album.Error) + } if v, ok := raw["title"]; ok { json.Unmarshal(v, &album.Title) } @@ -360,3 +364,61 @@ func (ti *TrailerInfo) DeList(data []byte, c ClientInterface) ([]Model, error) { } 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"` +} + diff --git a/internal/models/track.go b/internal/models/track.go index a389a7c..767c43e 100644 --- a/internal/models/track.go +++ b/internal/models/track.go @@ -8,24 +8,68 @@ import ( // Track represents a full track from the Yandex Music API. type Track struct { BaseModel - ID JSONID `json:"id"` - Title string `json:"title"` - DurationMs int `json:"duration_ms"` - StorageDir string `json:"storage_dir"` - CoverURI string `json:"cover_uri"` - Artists []Artist `json:"artists"` - Albums []Album `json:"albums"` - Genre string `json:"genre"` - Version string `json:"version"` - Explicit bool `json:"explicit"` - Available bool `json:"available"` - AvailableForPremiumUsers bool `json:"available_for_premium_users"` - R128 *R128 `json:"r128"` - TrackSharingFlag string `json:"track_sharing_flag"` - Major *Major `json:"major"` - LyricsInfo *TrackLyricsInfo `json:"lyrics_info"` - SimilarTracks []Track `json:"similar_tracks"` - DownloadInfo []DownloadInfo `json:"download_info"` + ID JSONID `json:"id"` + Title string `json:"title"` + Available bool `json:"available"` + Artists []Artist `json:"artists"` + Albums []Album `json:"albums"` + AvailableForPremiumUsers bool `json:"available_for_premium_users"` + LyricsAvailable bool `json:"lyrics_available"` + 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"` + 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"` + 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"` } // DeJSON deserializes a Track from JSON data. @@ -74,6 +118,12 @@ func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) { if v, ok := raw["track_sharing_flag"]; ok { json.Unmarshal(v, &track.TrackSharingFlag) } + if v, ok := raw["available_as_ringtone"]; ok { + json.Unmarshal(v, &track.AvailableAsRingtone) + } + if v, ok := raw["ringtone_id"]; ok { + json.Unmarshal(v, &track.RingtoneID) + } // Nested objects if v, ok := raw["artists"]; ok { @@ -112,6 +162,12 @@ func (t *Track) DeJSON(data []byte, c ClientInterface) (Model, error) { 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 } @@ -144,9 +200,6 @@ func (t *Track) GetDownloadInfo(quality string) *DownloadInfo { 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. type Major struct { ID JSONID `json:"id"` @@ -154,76 +207,6 @@ type Major struct { Description string `json:"description"` } -// Lyrics represents track lyrics from supplement endpoint. -type Lyrics struct { - BaseModel - ID JSONID `json:"id"` - Text string `json:"lyrics"` - FullLyrics string `json:"full_lyrics"` - HasRights bool `json:"has_rights"` - ShowTranslation bool `json:"show_translation"` - TextLanguage string `json:"text_language"` - URL string `json:"url"` - Major *Major `json:"major"` -} - -// DeJSON deserializes Lyrics from JSON data. -func (l *Lyrics) DeJSON(data []byte, c ClientInterface) (Model, error) { - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - lyrics := &Lyrics{BaseModel: *NewBaseModel(c)} - - if v, ok := raw["id"]; ok { - json.Unmarshal(v, &lyrics.ID) - } - if v, ok := raw["lyrics"]; ok { - json.Unmarshal(v, &lyrics.Text) - } - if v, ok := raw["full_lyrics"]; ok { - json.Unmarshal(v, &lyrics.FullLyrics) - } - if v, ok := raw["has_rights"]; ok { - json.Unmarshal(v, &lyrics.HasRights) - } - if v, ok := raw["show_translation"]; ok { - json.Unmarshal(v, &lyrics.ShowTranslation) - } - if v, ok := raw["text_language"]; ok { - json.Unmarshal(v, &lyrics.TextLanguage) - } - if v, ok := raw["url"]; ok { - json.Unmarshal(v, &lyrics.URL) - } - if v, ok := raw["major"]; ok { - var major Major - json.Unmarshal(v, &major) - lyrics.Major = &major - } - - return lyrics, nil -} - -// DeList deserializes a list of Lyrics from JSON data. -func (l *Lyrics) DeList(data []byte, c ClientInterface) ([]Model, error) { - var rawList []json.RawMessage - if err := json.Unmarshal(data, &rawList); err != nil { - return nil, err - } - - result := make([]Model, 0, len(rawList)) - for _, item := range rawList { - lyrics := &Lyrics{BaseModel: *NewBaseModel(c)} - if err := json.Unmarshal(item, lyrics); err != nil { - continue - } - result = append(result, lyrics) - } - return result, nil -} - // TrackLyricsInfo represents lyrics availability info for a track. type TrackLyricsInfo struct { BaseModel @@ -267,6 +250,7 @@ func (tli *TrackLyricsInfo) DeList(data []byte, c ClientInterface) ([]Model, err } return result, nil } + // DownloadInfo represents file download info for a track. type DownloadInfo struct { Quality string `json:"quality"` @@ -278,3 +262,276 @@ type DownloadInfo struct { 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 +}