From 277a3e69247849e1d287a2cd3bfb4bde8c457bdb Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Sun, 18 Oct 2026 12:39:45 +0300 Subject: [PATCH] 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 --- internal/models/album.go | 125 ++++++++++++++++++++++++++++------ internal/models/album_test.go | 57 +++++----------- 2 files changed, 124 insertions(+), 58 deletions(-) diff --git a/internal/models/album.go b/internal/models/album.go index 328df8e..73ce000 100644 --- a/internal/models/album.go +++ b/internal/models/album.go @@ -8,20 +8,20 @@ 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 []Label `json:"labels"` - Volumes [][]Track `json:"volumes"` - Description string `json:"description"` - Meta *AlbumMeta `json:"meta"` - Best *Best `json:"best"` + 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"` @@ -32,7 +32,93 @@ type Album struct { Options []string `json:"options"` TrackPositions []TrackPosition `json:"track_positions"` SimilarAlbums []Album `json:"similar_albums"` - Trailer *TrailerInfo `json:"trailer"` + Trailer *AlbumTrailerStatus `json:"trailer"` +} + +// 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:"-"` + IsStr bool `json:"-"` +} + +// 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 + } + 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 "" +} + +// 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. @@ -101,9 +187,10 @@ func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) { album.Artists = artists } if v, ok := raw["labels"]; ok { - var labels []Label - json.Unmarshal(v, &labels) - album.Labels = labels + var labels []LabelOrString + if err := json.Unmarshal(v, &labels); err == nil { + album.Labels = labels + } } if v, ok := raw["volumes"]; ok { var volumes [][]Track @@ -131,7 +218,7 @@ func (a *Album) DeJSON(data []byte, c ClientInterface) (Model, error) { album.SimilarAlbums = similar } if v, ok := raw["trailer"]; ok { - var trailer TrailerInfo + var trailer AlbumTrailerStatus json.Unmarshal(v, &trailer) album.Trailer = &trailer } diff --git a/internal/models/album_test.go b/internal/models/album_test.go index f7dff3e..e1e0ce9 100644 --- a/internal/models/album_test.go +++ b/internal/models/album_test.go @@ -30,7 +30,7 @@ func TestAlbum_DeJSON(t *testing.T) { "best": {"tracks": [{"id": "bt1", "title": "Best Track", "duration_ms": 200000}]}, "track_positions": [{"revision": 1, "timestamp": "2023-01-01T00:00:00Z", "position": 1, "from_cache": false}], "similar_albums": [{"id": "sim1", "title": "Similar Album", "cover_uri": "https://example.com/sim.jpg", "year": 2022}], - "trailer": {"preview_uri": "https://example.com/preview.mp3", "track_id": "t1", "album_id": "123", "duration": 30} + "trailer": {"available": true} }`) album := &Album{BaseModel: *NewBaseModel(client)} @@ -101,8 +101,8 @@ func TestAlbum_DeJSON(t *testing.T) { if len(resultAlbum.Labels) != 1 { t.Errorf("Expected 1 label, got %d", len(resultAlbum.Labels)) } - if resultAlbum.Labels[0].Name != "Label 1" { - t.Errorf("Expected label name 'Label 1', got '%s'", resultAlbum.Labels[0].Name) + if resultAlbum.Labels[0].String() != "Label 1" { + t.Errorf("Expected label name 'Label 1', got '%s'", resultAlbum.Labels[0].String()) } // Test volumes @@ -164,17 +164,8 @@ func TestAlbum_DeJSON(t *testing.T) { if resultAlbum.Trailer == nil { t.Fatal("Expected Trailer to be non-nil") } - if resultAlbum.Trailer.PreviewURI != "https://example.com/preview.mp3" { - t.Errorf("Expected PreviewURI 'https://example.com/preview.mp3', got '%s'", resultAlbum.Trailer.PreviewURI) - } - if resultAlbum.Trailer.TrackID != "t1" { - t.Errorf("Expected TrackID 't1', got '%s'", resultAlbum.Trailer.TrackID) - } - if resultAlbum.Trailer.AlbumID != "123" { - t.Errorf("Expected AlbumID '123', got '%s'", resultAlbum.Trailer.AlbumID) - } - if resultAlbum.Trailer.Duration != 30 { - t.Errorf("Expected Duration 30, got %d", resultAlbum.Trailer.Duration) + if !resultAlbum.Trailer.Available { + t.Errorf("Expected Trailer.Available true, got false") } } @@ -308,50 +299,38 @@ func TestAlbumMeta_DeList(t *testing.T) { } } -func TestTrailerInfo_DeJSON(t *testing.T) { +func TestAlbumTrailerStatus_DeJSON(t *testing.T) { client := newMockClient() data := []byte(`{ - "preview_uri": "https://example.com/preview.mp3", - "track_id": "track123", - "album_id": "album456", - "duration": 45 + "available": true }`) - trailer := &TrailerInfo{BaseModel: *NewBaseModel(client)} - result, err := trailer.DeJSON(data, client) + status := &AlbumTrailerStatus{BaseModel: *NewBaseModel(client)} + result, err := status.DeJSON(data, client) if err != nil { t.Fatalf("DeJSON failed: %v", err) } - resultTrailer := result.(*TrailerInfo) - if resultTrailer.PreviewURI != "https://example.com/preview.mp3" { - t.Errorf("Expected PreviewURI 'https://example.com/preview.mp3', got '%s'", resultTrailer.PreviewURI) - } - if resultTrailer.TrackID != "track123" { - t.Errorf("Expected TrackID 'track123', got '%s'", resultTrailer.TrackID) - } - if resultTrailer.AlbumID != "album456" { - t.Errorf("Expected AlbumID 'album456', got '%s'", resultTrailer.AlbumID) - } - if resultTrailer.Duration != 45 { - t.Errorf("Expected Duration 45, got %d", resultTrailer.Duration) + resultStatus := result.(*AlbumTrailerStatus) + if !resultStatus.Available { + t.Errorf("Expected Available true, got false") } } -func TestTrailerInfo_DeList(t *testing.T) { +func TestAlbumTrailerStatus_DeList(t *testing.T) { client := newMockClient() data := []byte(`[ - {"preview_uri": "https://example.com/preview1.mp3", "track_id": "t1", "album_id": "a1", "duration": 30}, - {"preview_uri": "https://example.com/preview2.mp3", "track_id": "t2", "album_id": "a2", "duration": 45} + {"available": true}, + {"available": false} ]`) - trailer := &TrailerInfo{BaseModel: *NewBaseModel(client)} - result, err := trailer.DeList(data, client) + status := &AlbumTrailerStatus{BaseModel: *NewBaseModel(client)} + result, err := status.DeList(data, client) if err != nil { t.Fatalf("DeList failed: %v", err) } if len(result) != 2 { - t.Errorf("Expected 2 trailers, got %d", len(result)) + t.Errorf("Expected 2 statuses, got %d", len(result)) } }