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
This commit is contained in:
Mrixs committed 2026-10-18 12:39:45 +03:00
1 parent 6312c24b0f
commit 277a3e6924
2 files changed
+124 -58

No files matched your search

+106 -19
View File
@@ -8,20 +8,20 @@ import (
// Album represents a full album from the Yandex Music API. // Album represents a full album from the Yandex Music API.
type Album struct { type Album struct {
BaseModel BaseModel
ID JSONID `json:"id"` ID JSONID `json:"id"`
Title string `json:"title"` Title string `json:"title"`
CoverURI string `json:"cover_uri"` CoverURI string `json:"cover_uri"`
Genre string `json:"genre"` Genre string `json:"genre"`
Year int `json:"year"` Year int `json:"year"`
TrackCount int `json:"track_count"` TrackCount int `json:"track_count"`
Available bool `json:"available"` Available bool `json:"available"`
ReleaseDate string `json:"release_date"` 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"` Volumes [][]Track `json:"volumes"`
Description string `json:"description"` Description string `json:"description"`
Meta *AlbumMeta `json:"meta"` Meta *AlbumMeta `json:"meta"`
Best *Best `json:"best"` Best *Best `json:"best"`
// Additional fields // Additional fields
Region string `json:"region"` Region string `json:"region"`
AvailableForPremiumUsers bool `json:"available_for_premium_users"` AvailableForPremiumUsers bool `json:"available_for_premium_users"`
@@ -32,7 +32,93 @@ type Album struct {
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"` 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. // DeJSON deserializes an Album from JSON data.
@@ -101,9 +187,10 @@ 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
@@ -131,7 +218,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
} }
+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))
} }
} }