mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
🔥 NEW ENDPOINTS IMPLEMENTED: 📊 /introspect endpoint: - Get detailed music service state and capabilities data - Support for SPOTIFY, PANDORA, TUNEIN, AMAZON, DEEZER services - Service state tracking (Active, Inactive, InactiveUnselected) - Playback capabilities (skip, seek, resume, data collection) - Authentication token status and user account information - Subscription type and content history metadata 📚 /recents endpoint: - Retrieve recently played content history - Support for all music sources (Spotify, Local, TuneIn, Pandora, etc.) - Rich filtering by source type and content type - Content classification (tracks, stations, playlists, albums) - Presetable item identification and artwork metadata - Timestamp tracking with UTC time support ⚡ CLIENT API: - client.Introspect(source, sourceAccount) method - client.IntrospectSpotify(sourceAccount) convenience method - client.GetRecents() method with comprehensive filtering - Complete error handling and validation - Rich helper methods for content analysis 🖥️ CLI COMMANDS: - soundtouch-cli source introspect --source <SERVICE> - soundtouch-cli source introspect-spotify - soundtouch-cli source introspect-all (bulk introspect) - soundtouch-cli recents list [--detailed] [--limit N] - soundtouch-cli recents filter --source <SRC> --type <TYPE> - soundtouch-cli recents latest (most recent item) - soundtouch-cli recents stats (detailed analytics) 📦 MODELS & FEATURES: - IntrospectRequest/Response with service-specific handling - RecentsResponse with RecentsResponseItem for individual items - Rich filtering: GetSpotifyItems(), GetTracks(), GetPresetableItems() - Content type detection: IsTrack(), IsStation(), IsPlaylist() - Source classification: IsStreamingContent(), IsLocalContent() - Full XML marshalling/unmarshalling with proper attribute handling 🧪 COMPREHENSIVE TESTING: - Unit tests for models with XML parsing validation - Integration tests for real device communication - CLI command tests with mock server responses - Error condition testing and edge case handling - Performance tests and timeout validation 📖 DOCUMENTATION & EXAMPLES: - Updated API endpoints overview marking endpoints as implemented - Comprehensive CLI reference with usage examples - Removed endpoints from unimplemented list - Updated wiki implementation plan status - Complete example applications with README guides - Real-world usage patterns and best practices ✨ KEY FEATURES: - Service health monitoring and diagnostics - Recently played content discovery and analysis - Preset candidate identification - Content statistics and usage analytics - Time-based filtering and relative timestamps - Rich emoji-based CLI output formatting - Cross-service compatibility and error handling This implements two critical missing endpoints from the SoundTouch API, providing essential functionality for music service management and recently played content analysis with full programmatic and CLI access.
581 lines
16 KiB
Go
581 lines
16 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"testing"
|
|
)
|
|
|
|
func TestRecentsResponse_Unmarshal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
xmlData string
|
|
expected *RecentsResponse
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "complete recents response",
|
|
xmlData: `<recents>
|
|
<recent deviceID="1004567890AA" utcTime="1701202831">
|
|
<contentItem source="STORED_MUSIC" location="6_a2874b5d_4f83d999" sourceAccount="d09708a1-5953-44bc-a413-123456789012/0" isPresetable="true">
|
|
<itemName>MercyMe, It's Christmas!</itemName>
|
|
</contentItem>
|
|
</recent>
|
|
<recent deviceID="1004567890AA" utcTime="1700232917" id="2487503626">
|
|
<contentItem source="LOCAL_MUSIC" type="track" location="track:2590" sourceAccount="3f205110-4a57-4e91-810a-123456789012" isPresetable="true">
|
|
<itemName>Baby It's Cold Outside - ANNE MURRAY</itemName>
|
|
</contentItem>
|
|
</recent>
|
|
</recents>`,
|
|
expected: &RecentsResponse{
|
|
Items: []RecentsResponseItem{
|
|
{
|
|
DeviceID: "1004567890AA",
|
|
UTCTime: 1701202831,
|
|
ContentItem: &ContentItem{
|
|
Source: "STORED_MUSIC",
|
|
Location: "6_a2874b5d_4f83d999",
|
|
SourceAccount: "d09708a1-5953-44bc-a413-123456789012/0",
|
|
IsPresetable: true,
|
|
ItemName: "MercyMe, It's Christmas!",
|
|
},
|
|
},
|
|
{
|
|
DeviceID: "1004567890AA",
|
|
UTCTime: 1700232917,
|
|
ID: "2487503626",
|
|
ContentItem: &ContentItem{
|
|
Source: "LOCAL_MUSIC",
|
|
Type: "track",
|
|
Location: "track:2590",
|
|
SourceAccount: "3f205110-4a57-4e91-810a-123456789012",
|
|
IsPresetable: true,
|
|
ItemName: "Baby It's Cold Outside - ANNE MURRAY",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "spotify recent item",
|
|
xmlData: `<recents>
|
|
<recent deviceID="1004567890AA" utcTime="1701300000" id="spotify123">
|
|
<contentItem source="SPOTIFY" type="track" location="spotify:track:4iV5W9uYEdYUVa79Axb7Rh" sourceAccount="spotify_user" isPresetable="true">
|
|
<itemName>Shape of You - Ed Sheeran</itemName>
|
|
<containerArt>https://i.scdn.co/image/ab67616d0000b273ba5db46f4b838ef6027e6f96</containerArt>
|
|
</contentItem>
|
|
</recent>
|
|
</recents>`,
|
|
expected: &RecentsResponse{
|
|
Items: []RecentsResponseItem{
|
|
{
|
|
DeviceID: "1004567890AA",
|
|
UTCTime: 1701300000,
|
|
ID: "spotify123",
|
|
ContentItem: &ContentItem{
|
|
Source: "SPOTIFY",
|
|
Type: "track",
|
|
Location: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
|
|
SourceAccount: "spotify_user",
|
|
IsPresetable: true,
|
|
ItemName: "Shape of You - Ed Sheeran",
|
|
ContainerArt: "https://i.scdn.co/image/ab67616d0000b273ba5db46f4b838ef6027e6f96",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty recents",
|
|
xmlData: `<recents>
|
|
</recents>`,
|
|
expected: &RecentsResponse{
|
|
Items: []RecentsResponseItem{},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var response RecentsResponse
|
|
err := xml.Unmarshal([]byte(tt.xmlData), &response)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
// Compare basic structure
|
|
if len(response.Items) != len(tt.expected.Items) {
|
|
t.Errorf("expected %d items, got %d", len(tt.expected.Items), len(response.Items))
|
|
}
|
|
|
|
// Compare each item
|
|
for i, expectedItem := range tt.expected.Items {
|
|
if i >= len(response.Items) {
|
|
break
|
|
}
|
|
|
|
actualItem := response.Items[i]
|
|
|
|
if actualItem.DeviceID != expectedItem.DeviceID {
|
|
t.Errorf("item %d: expected deviceID %s, got %s", i, expectedItem.DeviceID, actualItem.DeviceID)
|
|
}
|
|
if actualItem.UTCTime != expectedItem.UTCTime {
|
|
t.Errorf("item %d: expected utcTime %d, got %d", i, expectedItem.UTCTime, actualItem.UTCTime)
|
|
}
|
|
if actualItem.ID != expectedItem.ID {
|
|
t.Errorf("item %d: expected id %s, got %s", i, expectedItem.ID, actualItem.ID)
|
|
}
|
|
|
|
// Compare ContentItem
|
|
if expectedItem.ContentItem != nil {
|
|
if actualItem.ContentItem == nil {
|
|
t.Errorf("item %d: expected contentItem, got nil", i)
|
|
continue
|
|
}
|
|
|
|
if actualItem.ContentItem.Source != expectedItem.ContentItem.Source {
|
|
t.Errorf("item %d: expected source %s, got %s", i, expectedItem.ContentItem.Source, actualItem.ContentItem.Source)
|
|
}
|
|
if actualItem.ContentItem.Type != expectedItem.ContentItem.Type {
|
|
t.Errorf("item %d: expected type %s, got %s", i, expectedItem.ContentItem.Type, actualItem.ContentItem.Type)
|
|
}
|
|
if actualItem.ContentItem.Location != expectedItem.ContentItem.Location {
|
|
t.Errorf("item %d: expected location %s, got %s", i, expectedItem.ContentItem.Location, actualItem.ContentItem.Location)
|
|
}
|
|
if actualItem.ContentItem.ItemName != expectedItem.ContentItem.ItemName {
|
|
t.Errorf("item %d: expected itemName %s, got %s", i, expectedItem.ContentItem.ItemName, actualItem.ContentItem.ItemName)
|
|
}
|
|
if actualItem.ContentItem.IsPresetable != expectedItem.ContentItem.IsPresetable {
|
|
t.Errorf("item %d: expected isPresetable %t, got %t", i, expectedItem.ContentItem.IsPresetable, actualItem.ContentItem.IsPresetable)
|
|
}
|
|
} else if actualItem.ContentItem != nil {
|
|
t.Errorf("item %d: expected nil contentItem, got non-nil", i)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecentsResponse_Methods(t *testing.T) {
|
|
response := &RecentsResponse{
|
|
Items: []RecentsResponseItem{
|
|
{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701200000,
|
|
ContentItem: &ContentItem{
|
|
Source: "SPOTIFY",
|
|
Type: "track",
|
|
ItemName: "Song 1",
|
|
IsPresetable: true,
|
|
},
|
|
},
|
|
{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701100000,
|
|
ContentItem: &ContentItem{
|
|
Source: "LOCAL_MUSIC",
|
|
Type: "track",
|
|
ItemName: "Song 2",
|
|
IsPresetable: false,
|
|
},
|
|
},
|
|
{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701000000,
|
|
ContentItem: &ContentItem{
|
|
Source: "TUNEIN",
|
|
Type: "stationurl",
|
|
ItemName: "Radio Station",
|
|
IsPresetable: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test GetItemCount
|
|
if response.GetItemCount() != 3 {
|
|
t.Errorf("expected item count 3, got %d", response.GetItemCount())
|
|
}
|
|
|
|
// Test IsEmpty
|
|
if response.IsEmpty() {
|
|
t.Error("expected IsEmpty() to return false")
|
|
}
|
|
|
|
// Test GetMostRecent
|
|
mostRecent := response.GetMostRecent()
|
|
if mostRecent == nil {
|
|
t.Error("expected most recent item, got nil")
|
|
} else if mostRecent.UTCTime != 1701200000 {
|
|
t.Errorf("expected most recent UTCTime 1701200000, got %d", mostRecent.UTCTime)
|
|
}
|
|
|
|
// Test GetItemsBySource
|
|
spotifyItems := response.GetItemsBySource("SPOTIFY")
|
|
if len(spotifyItems) != 1 {
|
|
t.Errorf("expected 1 Spotify item, got %d", len(spotifyItems))
|
|
}
|
|
|
|
localItems := response.GetItemsBySource("LOCAL_MUSIC")
|
|
if len(localItems) != 1 {
|
|
t.Errorf("expected 1 LOCAL_MUSIC item, got %d", len(localItems))
|
|
}
|
|
|
|
// Test GetSpotifyItems
|
|
spotifyItems2 := response.GetSpotifyItems()
|
|
if len(spotifyItems2) != 1 {
|
|
t.Errorf("expected 1 Spotify item, got %d", len(spotifyItems2))
|
|
}
|
|
|
|
// Test GetPresetableItems
|
|
presetableItems := response.GetPresetableItems()
|
|
if len(presetableItems) != 2 {
|
|
t.Errorf("expected 2 presetable items, got %d", len(presetableItems))
|
|
}
|
|
|
|
// Test GetTracks
|
|
tracks := response.GetTracks()
|
|
if len(tracks) != 2 {
|
|
t.Errorf("expected 2 track items, got %d", len(tracks))
|
|
}
|
|
|
|
// Test GetStations
|
|
stations := response.GetStations()
|
|
if len(stations) != 1 {
|
|
t.Errorf("expected 1 station item, got %d", len(stations))
|
|
}
|
|
}
|
|
|
|
func TestRecentsResponse_EmptyResponse(t *testing.T) {
|
|
response := &RecentsResponse{
|
|
Items: []RecentsResponseItem{},
|
|
}
|
|
|
|
// Test empty response methods
|
|
if response.GetItemCount() != 0 {
|
|
t.Errorf("expected item count 0, got %d", response.GetItemCount())
|
|
}
|
|
|
|
if !response.IsEmpty() {
|
|
t.Error("expected IsEmpty() to return true")
|
|
}
|
|
|
|
if response.GetMostRecent() != nil {
|
|
t.Error("expected GetMostRecent() to return nil")
|
|
}
|
|
|
|
if len(response.GetSpotifyItems()) != 0 {
|
|
t.Errorf("expected 0 Spotify items, got %d", len(response.GetSpotifyItems()))
|
|
}
|
|
}
|
|
|
|
func TestRecentItem_Methods(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
item RecentsResponseItem
|
|
test func(t *testing.T, item *RecentsResponseItem)
|
|
}{
|
|
{
|
|
name: "spotify track item",
|
|
item: RecentsResponseItem{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701200000,
|
|
ID: "spotify123",
|
|
ContentItem: &ContentItem{
|
|
Source: "SPOTIFY",
|
|
Type: "track",
|
|
Location: "spotify:track:123",
|
|
SourceAccount: "user@spotify.com",
|
|
IsPresetable: true,
|
|
ItemName: "Test Song",
|
|
ContainerArt: "https://example.com/art.jpg",
|
|
},
|
|
},
|
|
test: func(t *testing.T, item *RecentsResponseItem) {
|
|
if !item.HasContent() {
|
|
t.Error("expected HasContent() to return true")
|
|
}
|
|
if item.GetDisplayName() != "Test Song" {
|
|
t.Errorf("expected display name 'Test Song', got %s", item.GetDisplayName())
|
|
}
|
|
if item.GetSource() != "SPOTIFY" {
|
|
t.Errorf("expected source 'SPOTIFY', got %s", item.GetSource())
|
|
}
|
|
if !item.IsTrack() {
|
|
t.Error("expected IsTrack() to return true")
|
|
}
|
|
if !item.IsSpotifyContent() {
|
|
t.Error("expected IsSpotifyContent() to return true")
|
|
}
|
|
if !item.IsStreamingContent() {
|
|
t.Error("expected IsStreamingContent() to return true")
|
|
}
|
|
if item.IsLocalContent() {
|
|
t.Error("expected IsLocalContent() to return false")
|
|
}
|
|
if !item.IsPresetable() {
|
|
t.Error("expected IsPresetable() to return true")
|
|
}
|
|
if !item.HasArtwork() {
|
|
t.Error("expected HasArtwork() to return true")
|
|
}
|
|
if item.GetArtwork() != "https://example.com/art.jpg" {
|
|
t.Errorf("expected artwork URL, got %s", item.GetArtwork())
|
|
}
|
|
if item.GetUTCTime() != 1701200000 {
|
|
t.Errorf("expected UTC time 1701200000, got %d", item.GetUTCTime())
|
|
}
|
|
if !item.HasID() {
|
|
t.Error("expected HasID() to return true")
|
|
}
|
|
if item.GetID() != "spotify123" {
|
|
t.Errorf("expected ID 'spotify123', got %s", item.GetID())
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "local music item",
|
|
item: RecentsResponseItem{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701100000,
|
|
ContentItem: &ContentItem{
|
|
Source: "LOCAL_MUSIC",
|
|
Type: "track",
|
|
Location: "/music/song.mp3",
|
|
IsPresetable: false,
|
|
ItemName: "Local Song",
|
|
},
|
|
},
|
|
test: func(t *testing.T, item *RecentsResponseItem) {
|
|
if !item.IsLocalContent() {
|
|
t.Error("expected IsLocalContent() to return true")
|
|
}
|
|
if item.IsStreamingContent() {
|
|
t.Error("expected IsStreamingContent() to return false")
|
|
}
|
|
if item.IsSpotifyContent() {
|
|
t.Error("expected IsSpotifyContent() to return false")
|
|
}
|
|
if item.HasArtwork() {
|
|
t.Error("expected HasArtwork() to return false")
|
|
}
|
|
if item.GetArtwork() != "" {
|
|
t.Errorf("expected empty artwork, got %s", item.GetArtwork())
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "radio station item",
|
|
item: RecentsResponseItem{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701000000,
|
|
ContentItem: &ContentItem{
|
|
Source: "TUNEIN",
|
|
Type: "stationurl",
|
|
Location: "tunein:station:123",
|
|
IsPresetable: true,
|
|
ItemName: "Rock FM",
|
|
},
|
|
},
|
|
test: func(t *testing.T, item *RecentsResponseItem) {
|
|
if !item.IsStation() {
|
|
t.Error("expected IsStation() to return true")
|
|
}
|
|
if item.IsTrack() {
|
|
t.Error("expected IsTrack() to return false")
|
|
}
|
|
if !item.IsStreamingContent() {
|
|
t.Error("expected IsStreamingContent() to return true")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "empty content item",
|
|
item: RecentsResponseItem{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701000000,
|
|
},
|
|
test: func(t *testing.T, item *RecentsResponseItem) {
|
|
if item.HasContent() {
|
|
t.Error("expected HasContent() to return false")
|
|
}
|
|
if item.GetDisplayName() != "Unknown Item" {
|
|
t.Errorf("expected display name 'Unknown Item', got %s", item.GetDisplayName())
|
|
}
|
|
if item.GetSource() != "" {
|
|
t.Errorf("expected empty source, got %s", item.GetSource())
|
|
}
|
|
if item.IsTrack() {
|
|
t.Error("expected IsTrack() to return false")
|
|
}
|
|
if item.IsPresetable() {
|
|
t.Error("expected IsPresetable() to return false")
|
|
}
|
|
if item.HasID() {
|
|
t.Error("expected HasID() to return false")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.test(t, &tt.item)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecentItem_ContentTypes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
contentType string
|
|
expected map[string]bool
|
|
}{
|
|
{
|
|
name: "track type",
|
|
contentType: "track",
|
|
expected: map[string]bool{
|
|
"IsTrack": true,
|
|
"IsStation": false,
|
|
"IsPlaylist": false,
|
|
"IsAlbum": false,
|
|
"IsContainer": false,
|
|
},
|
|
},
|
|
{
|
|
name: "station type",
|
|
contentType: "stationurl",
|
|
expected: map[string]bool{
|
|
"IsTrack": false,
|
|
"IsStation": true,
|
|
"IsPlaylist": false,
|
|
"IsAlbum": false,
|
|
"IsContainer": false,
|
|
},
|
|
},
|
|
{
|
|
name: "playlist type",
|
|
contentType: "playlist",
|
|
expected: map[string]bool{
|
|
"IsTrack": false,
|
|
"IsStation": false,
|
|
"IsPlaylist": true,
|
|
"IsAlbum": false,
|
|
"IsContainer": false,
|
|
},
|
|
},
|
|
{
|
|
name: "album type",
|
|
contentType: "album",
|
|
expected: map[string]bool{
|
|
"IsTrack": false,
|
|
"IsStation": false,
|
|
"IsPlaylist": false,
|
|
"IsAlbum": true,
|
|
"IsContainer": false,
|
|
},
|
|
},
|
|
{
|
|
name: "container type",
|
|
contentType: "container",
|
|
expected: map[string]bool{
|
|
"IsTrack": false,
|
|
"IsStation": false,
|
|
"IsPlaylist": false,
|
|
"IsAlbum": false,
|
|
"IsContainer": true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
item := RecentsResponseItem{
|
|
ContentItem: &ContentItem{
|
|
Type: tt.contentType,
|
|
},
|
|
}
|
|
|
|
results := map[string]bool{
|
|
"IsTrack": item.IsTrack(),
|
|
"IsStation": item.IsStation(),
|
|
"IsPlaylist": item.IsPlaylist(),
|
|
"IsAlbum": item.IsAlbum(),
|
|
"IsContainer": item.IsContainer(),
|
|
}
|
|
|
|
for method, expected := range tt.expected {
|
|
if results[method] != expected {
|
|
t.Errorf("expected %s() to return %t, got %t", method, expected, results[method])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecentsResponse_FilterMethods(t *testing.T) {
|
|
response := &RecentsResponse{
|
|
Items: []RecentsResponseItem{
|
|
{
|
|
ContentItem: &ContentItem{Source: "SPOTIFY", Type: "track"},
|
|
},
|
|
{
|
|
ContentItem: &ContentItem{Source: "PANDORA", Type: "track"},
|
|
},
|
|
{
|
|
ContentItem: &ContentItem{Source: "LOCAL_MUSIC", Type: "track"},
|
|
},
|
|
{
|
|
ContentItem: &ContentItem{Source: "STORED_MUSIC", Type: "track"},
|
|
},
|
|
{
|
|
ContentItem: &ContentItem{Source: "TUNEIN", Type: "stationurl"},
|
|
},
|
|
{
|
|
ContentItem: &ContentItem{Source: "SPOTIFY", Type: "playlist"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test individual service filters
|
|
if len(response.GetSpotifyItems()) != 2 {
|
|
t.Errorf("expected 2 Spotify items, got %d", len(response.GetSpotifyItems()))
|
|
}
|
|
|
|
if len(response.GetPandoraItems()) != 1 {
|
|
t.Errorf("expected 1 Pandora item, got %d", len(response.GetPandoraItems()))
|
|
}
|
|
|
|
if len(response.GetLocalMusicItems()) != 1 {
|
|
t.Errorf("expected 1 LOCAL_MUSIC item, got %d", len(response.GetLocalMusicItems()))
|
|
}
|
|
|
|
if len(response.GetStoredMusicItems()) != 1 {
|
|
t.Errorf("expected 1 STORED_MUSIC item, got %d", len(response.GetStoredMusicItems()))
|
|
}
|
|
|
|
if len(response.GetTuneInItems()) != 1 {
|
|
t.Errorf("expected 1 TuneIn item, got %d", len(response.GetTuneInItems()))
|
|
}
|
|
|
|
// Test type filters
|
|
if len(response.GetTracks()) != 4 {
|
|
t.Errorf("expected 4 track items, got %d", len(response.GetTracks()))
|
|
}
|
|
|
|
if len(response.GetStations()) != 1 {
|
|
t.Errorf("expected 1 station item, got %d", len(response.GetStations()))
|
|
}
|
|
|
|
if len(response.GetPlaylistsAndAlbums()) != 1 {
|
|
t.Errorf("expected 1 playlist/album item, got %d", len(response.GetPlaylistsAndAlbums()))
|
|
}
|
|
}
|