diff --git a/cmd/soundtouch-cli/cmd_preset.go b/cmd/soundtouch-cli/cmd_preset.go new file mode 100644 index 0000000..8bfc8dc --- /dev/null +++ b/cmd/soundtouch-cli/cmd_preset.go @@ -0,0 +1,218 @@ +package main + +import ( + "fmt" + + "github.com/gesellix/bose-soundtouch/pkg/models" + "github.com/urfave/cli/v2" +) + +// storeCurrentPreset handles storing currently playing content as preset +func storeCurrentPreset(c *cli.Context) error { + slot := c.Int("slot") + clientConfig := GetClientConfig(c) + + PrintDeviceHeader(fmt.Sprintf("Storing current content as preset %d", slot), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Check what's currently playing + nowPlaying, err := client.GetNowPlaying() + if err != nil { + PrintError(fmt.Sprintf("Failed to get current content: %v", err)) + return err + } + + if nowPlaying.IsEmpty() { + PrintError("No content currently playing") + return fmt.Errorf("no content currently playing") + } + + if nowPlaying.ContentItem == nil { + PrintError("Current content has no preset information") + return fmt.Errorf("current content cannot be saved as preset") + } + + if !nowPlaying.ContentItem.IsPresetable { + PrintError("Current content cannot be saved as preset") + fmt.Printf(" Content: %s\n", nowPlaying.Track) + fmt.Printf(" Source: %s\n", nowPlaying.Source) + return fmt.Errorf("current content cannot be preset") + } + + // Show what we're about to store + fmt.Printf("Current Content:\n") + fmt.Printf(" Track: %s\n", nowPlaying.Track) + if nowPlaying.Artist != "" { + fmt.Printf(" Artist: %s\n", nowPlaying.Artist) + } + if nowPlaying.Album != "" { + fmt.Printf(" Album: %s\n", nowPlaying.Album) + } + fmt.Printf(" Source: %s\n", nowPlaying.Source) + if nowPlaying.ContentItem.Location != "" { + fmt.Printf(" Location: %s\n", nowPlaying.ContentItem.Location) + } + + // Store as preset + err = client.StoreCurrentAsPreset(slot) + if err != nil { + PrintError(fmt.Sprintf("Failed to store preset: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Stored current content as preset %d", slot)) + + return nil +} + +// storePreset handles storing specific content as preset +func storePreset(c *cli.Context) error { + slot := c.Int("slot") + source := c.String("source") + location := c.String("location") + sourceAccount := c.String("source-account") + name := c.String("name") + itemType := c.String("type") + artwork := c.String("artwork") + + clientConfig := GetClientConfig(c) + + if source == "" { + return fmt.Errorf("source is required (use --source)") + } + + if location == "" { + return fmt.Errorf("location is required (use --location)") + } + + PrintDeviceHeader(fmt.Sprintf("Storing %s content as preset %d", source, slot), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Create content item + contentItem := &models.ContentItem{ + Source: source, + Type: itemType, + Location: location, + SourceAccount: sourceAccount, + IsPresetable: true, + ItemName: name, + ContainerArt: artwork, + } + + // Set default type if not specified + if itemType == "" { + switch source { + case "SPOTIFY": + contentItem.Type = "uri" + case "TUNEIN", "LOCAL_INTERNET_RADIO": + contentItem.Type = "stationurl" + default: + contentItem.Type = "" + } + } + + // Show what we're storing + fmt.Printf("Content to store:\n") + fmt.Printf(" Name: %s\n", name) + fmt.Printf(" Source: %s\n", source) + fmt.Printf(" Location: %s\n", location) + if sourceAccount != "" { + fmt.Printf(" Source Account: %s\n", sourceAccount) + } + if itemType != "" { + fmt.Printf(" Type: %s\n", itemType) + } + + // Store preset + err = client.StorePreset(slot, contentItem) + if err != nil { + PrintError(fmt.Sprintf("Failed to store preset: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Stored content as preset %d", slot)) + + return nil +} + +// removePreset handles removing a preset +func removePreset(c *cli.Context) error { + slot := c.Int("slot") + clientConfig := GetClientConfig(c) + + PrintDeviceHeader(fmt.Sprintf("Removing preset %d", slot), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Check if preset exists first + presets, err := client.GetPresets() + if err != nil { + PrintError(fmt.Sprintf("Failed to get presets: %v", err)) + return err + } + + preset := presets.GetPresetByID(slot) + if preset == nil || preset.IsEmpty() { + PrintError(fmt.Sprintf("Preset %d is already empty", slot)) + return fmt.Errorf("preset %d does not exist", slot) + } + + // Show what we're removing + fmt.Printf("Removing preset %d:\n", slot) + fmt.Printf(" Name: %s\n", preset.GetDisplayName()) + fmt.Printf(" Source: %s\n", preset.GetSource()) + + // Remove preset + err = client.RemovePreset(slot) + if err != nil { + PrintError(fmt.Sprintf("Failed to remove preset: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Removed preset %d", slot)) + + return nil +} + +// selectPresetNew handles selecting a preset (new version that works with subcommands) +func selectPresetNew(c *cli.Context) error { + slot := c.Int("slot") + clientConfig := GetClientConfig(c) + + PrintDeviceHeader(fmt.Sprintf("Selecting preset %d", slot), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + err = client.SelectPreset(slot) + if err != nil { + PrintError(fmt.Sprintf("Failed to select preset: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Preset %d selected", slot)) + + return nil +} + +// listPresets handles listing all presets (alias for existing getPresets command) +func listPresets(c *cli.Context) error { + return getPresets(c) +} diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index 14cccfd..bac2be6 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -237,17 +237,94 @@ func main() { }, // Preset commands { - Name: "preset", - Usage: "Select preset by number", - Action: selectPreset, - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "preset", - Usage: "Preset number (1-6)", - Required: true, + Name: "preset", + Usage: "Preset management commands", + Subcommands: []*cli.Command{ + { + Name: "store-current", + Usage: "Store currently playing content as preset", + Action: storeCurrentPreset, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "slot", + Usage: "Preset slot number (1-6)", + Required: true, + }, + }, + Before: RequireHost, + }, + { + Name: "store", + Usage: "Store specific content as preset", + Action: storePreset, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "slot", + Usage: "Preset slot number (1-6)", + Required: true, + }, + &cli.StringFlag{ + Name: "source", + Usage: "Content source (SPOTIFY, TUNEIN, LOCAL_INTERNET_RADIO, etc.)", + Required: true, + }, + &cli.StringFlag{ + Name: "location", + Usage: "Content location (URI, URL, or ID)", + Required: true, + }, + &cli.StringFlag{ + Name: "source-account", + Usage: "Source account (username, device ID, etc.)", + }, + &cli.StringFlag{ + Name: "name", + Usage: "Display name for the preset", + }, + &cli.StringFlag{ + Name: "type", + Usage: "Content type (uri, stationurl, etc.)", + }, + &cli.StringFlag{ + Name: "artwork", + Usage: "Artwork URL", + }, + }, + Before: RequireHost, + }, + { + Name: "remove", + Usage: "Remove a preset", + Action: removePreset, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "slot", + Usage: "Preset slot number (1-6)", + Required: true, + }, + }, + Before: RequireHost, + }, + { + Name: "select", + Usage: "Select and play a preset", + Action: selectPresetNew, + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "slot", + Usage: "Preset slot number (1-6)", + Required: true, + }, + }, + Before: RequireHost, + }, + { + Name: "list", + Usage: "List all presets", + Action: listPresets, + Before: RequireHost, }, }, - Before: RequireHost, }, // Key commands { diff --git a/cmd/websocket-demo/main.go b/cmd/websocket-demo/main.go index a54bb27..26fe14d 100644 --- a/cmd/websocket-demo/main.go +++ b/cmd/websocket-demo/main.go @@ -342,17 +342,25 @@ func handleConnection(event *models.ConnectionStateUpdatedEvent) { } func handlePreset(event *models.PresetUpdatedEvent, verbose bool) { - preset := &event.Preset - fmt.Printf("\nšŸ“» Preset Update [%s]:\n", event.DeviceID) - fmt.Printf(" šŸ“» Preset: %d\n", preset.ID) + presets := &event.Presets + deviceHeader := "\nšŸ“» Presets Update" + if event.DeviceID != "" { + deviceHeader += fmt.Sprintf(" [%s]", event.DeviceID) + } + fmt.Printf("%s:\n", deviceHeader) + fmt.Printf(" šŸ“» Total presets: %d\n", len(presets.Preset)) - if preset.ContentItem != nil { - fmt.Printf(" šŸŽµ %s\n", preset.ContentItem.ItemName) - fmt.Printf(" šŸ“» Source: %s\n", preset.ContentItem.Source) + for _, preset := range presets.Preset { + fmt.Printf(" šŸ“» Preset %d:", preset.ID) + if preset.ContentItem != nil { + fmt.Printf(" %s", preset.ContentItem.ItemName) + fmt.Printf(" (%s)", preset.ContentItem.Source) + } + fmt.Println() } if verbose { - fmt.Printf(" šŸ“± Raw preset data: ID=%d\n", preset.ID) + fmt.Printf(" šŸ“± Raw presets data: %d total presets\n", len(presets.Preset)) } } diff --git a/pkg/client/client.go b/pkg/client/client.go index f373bf9..3f90e61 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -316,6 +316,69 @@ func (c *Client) IsCurrentContentPresetable() (bool, error) { return nowPlaying.ContentItem.IsPresetable, nil } +// StorePreset saves content as a preset on the SoundTouch device +func (c *Client) StorePreset(id int, contentItem *models.ContentItem) error { + if id < 1 || id > 6 { + return fmt.Errorf("preset ID must be between 1 and 6, got %d", id) + } + + if contentItem == nil { + return fmt.Errorf("content item cannot be nil") + } + + now := time.Now().Unix() + preset := &models.Preset{ + ID: id, + CreatedOn: &now, + UpdatedOn: &now, + ContentItem: contentItem, + } + + err := c.post("/storePreset", preset) + if err != nil { + return fmt.Errorf("failed to store preset %d: %w", id, err) + } + + return nil +} + +// StoreCurrentAsPreset saves currently playing content as preset +func (c *Client) StoreCurrentAsPreset(id int) error { + if id < 1 || id > 6 { + return fmt.Errorf("preset ID must be between 1 and 6, got %d", id) + } + + nowPlaying, err := c.GetNowPlaying() + if err != nil { + return fmt.Errorf("failed to get current content: %w", err) + } + + if nowPlaying.IsEmpty() || nowPlaying.ContentItem == nil { + return fmt.Errorf("no content currently playing") + } + + if !nowPlaying.ContentItem.IsPresetable { + return fmt.Errorf("current content cannot be saved as preset") + } + + return c.StorePreset(id, nowPlaying.ContentItem) +} + +// RemovePreset deletes a preset from the SoundTouch device +func (c *Client) RemovePreset(id int) error { + if id < 1 || id > 6 { + return fmt.Errorf("preset ID must be between 1 and 6, got %d", id) + } + + preset := &models.Preset{ID: id} + err := c.post("/removePreset", preset) + if err != nil { + return fmt.Errorf("failed to remove preset %d: %w", id, err) + } + + return nil +} + // SendKey sends a key press command to the device (press followed by release) func (c *Client) SendKey(keyValue string) error { if !models.IsValidKey(keyValue) { diff --git a/pkg/client/preset_test.go b/pkg/client/preset_test.go new file mode 100644 index 0000000..73e90e6 --- /dev/null +++ b/pkg/client/preset_test.go @@ -0,0 +1,590 @@ +package client + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gesellix/bose-soundtouch/pkg/models" +) + +func TestClient_StorePreset(t *testing.T) { + tests := []struct { + name string + presetID int + contentItem *models.ContentItem + serverResponse string + serverStatus int + expectError bool + errorContains string + }{ + { + name: "store_spotify_playlist_success", + presetID: 1, + contentItem: &models.ContentItem{ + Source: "SPOTIFY", + Type: "uri", + Location: "spotify:playlist:37i9dQZF1DX0XUsuxWHRQd", + SourceAccount: "testuser", + IsPresetable: true, + ItemName: "My Playlist", + ContainerArt: "https://example.com/art.jpg", + }, + serverResponse: `My Playlist`, + serverStatus: http.StatusOK, + expectError: false, + }, + { + name: "store_tunein_radio_success", + presetID: 2, + contentItem: &models.ContentItem{ + Source: "TUNEIN", + Type: "stationurl", + Location: "/v1/playback/station/s33828", + IsPresetable: true, + ItemName: "K-LOVE Radio", + }, + serverResponse: `K-LOVE Radio`, + serverStatus: http.StatusOK, + expectError: false, + }, + { + name: "store_local_internet_radio_success", + presetID: 3, + contentItem: &models.ContentItem{ + Source: "LOCAL_INTERNET_RADIO", + Type: "stationurl", + Location: "https://stream.example.com/radio", + IsPresetable: true, + ItemName: "Custom Radio", + }, + serverResponse: `Custom Radio`, + serverStatus: http.StatusOK, + expectError: false, + }, + { + name: "invalid_preset_id_too_low", + presetID: 0, + contentItem: &models.ContentItem{Source: "SPOTIFY", Location: "test"}, + expectError: true, + errorContains: "preset ID must be between 1 and 6", + }, + { + name: "invalid_preset_id_too_high", + presetID: 7, + contentItem: &models.ContentItem{Source: "SPOTIFY", Location: "test"}, + expectError: true, + errorContains: "preset ID must be between 1 and 6", + }, + { + name: "nil_content_item", + presetID: 1, + contentItem: nil, + expectError: true, + errorContains: "content item cannot be nil", + }, + { + name: "server_error_response", + presetID: 1, + contentItem: &models.ContentItem{Source: "SPOTIFY", Location: "test"}, + serverResponse: `Invalid preset`, + serverStatus: http.StatusBadRequest, + expectError: true, + errorContains: "failed to store preset 1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify request method and endpoint + if r.Method != http.MethodPost { + t.Errorf("Expected POST request, got %s", r.Method) + } + if r.URL.Path != "/storePreset" { + t.Errorf("Expected /storePreset endpoint, got %s", r.URL.Path) + } + + // Verify content type + if r.Header.Get("Content-Type") != "application/xml" { + t.Errorf("Expected Content-Type application/xml, got %s", r.Header.Get("Content-Type")) + } + + // Return mock response + if tt.serverStatus != 0 { + w.WriteHeader(tt.serverStatus) + } + if tt.serverResponse != "" { + w.Write([]byte(tt.serverResponse)) + } + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + err := client.StorePreset(tt.presetID, tt.contentItem) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + } + }) + } +} + +func TestClient_RemovePreset(t *testing.T) { + tests := []struct { + name string + presetID int + serverResponse string + serverStatus int + expectError bool + errorContains string + }{ + { + name: "remove_preset_success", + presetID: 3, + serverResponse: ``, + serverStatus: http.StatusOK, + expectError: false, + }, + { + name: "invalid_preset_id_too_low", + presetID: 0, + expectError: true, + errorContains: "preset ID must be between 1 and 6", + }, + { + name: "invalid_preset_id_too_high", + presetID: 7, + expectError: true, + errorContains: "preset ID must be between 1 and 6", + }, + { + name: "server_error_response", + presetID: 1, + serverResponse: `Preset not found`, + serverStatus: http.StatusNotFound, + expectError: true, + errorContains: "failed to remove preset 1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify request method and endpoint + if r.Method != http.MethodPost { + t.Errorf("Expected POST request, got %s", r.Method) + } + if r.URL.Path != "/removePreset" { + t.Errorf("Expected /removePreset endpoint, got %s", r.URL.Path) + } + + // Return mock response + if tt.serverStatus != 0 { + w.WriteHeader(tt.serverStatus) + } + if tt.serverResponse != "" { + w.Write([]byte(tt.serverResponse)) + } + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + err := client.RemovePreset(tt.presetID) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + } + }) + } +} + +func TestClient_StoreCurrentAsPreset(t *testing.T) { + tests := []struct { + name string + presetID int + nowPlayingResponse string + nowPlayingStatus int + storePresetStatus int + expectError bool + errorContains string + }{ + { + name: "store_current_spotify_success", + presetID: 2, + nowPlayingResponse: ` + + + Test Track + + Test Track + Test Artist + PLAY_STATE +`, + nowPlayingStatus: http.StatusOK, + storePresetStatus: http.StatusOK, + expectError: false, + }, + { + name: "store_current_tunein_success", + presetID: 1, + nowPlayingResponse: ` + + + K-LOVE Radio + + K-LOVE Radio + PLAY_STATE +`, + nowPlayingStatus: http.StatusOK, + storePresetStatus: http.StatusOK, + expectError: false, + }, + { + name: "empty_now_playing", + presetID: 1, + nowPlayingResponse: ` + + +`, + nowPlayingStatus: http.StatusOK, + expectError: true, + errorContains: "no content currently playing", + }, + { + name: "content_not_presetable", + presetID: 1, + nowPlayingResponse: ` + + + Phone Audio + + Phone Audio + PLAY_STATE +`, + nowPlayingStatus: http.StatusOK, + expectError: true, + errorContains: "current content cannot be saved as preset", + }, + { + name: "no_content_item", + presetID: 1, + nowPlayingResponse: ` + + Unknown Track + PLAY_STATE +`, + nowPlayingStatus: http.StatusOK, + expectError: true, + errorContains: "no content currently playing", + }, + { + name: "now_playing_request_fails", + presetID: 1, + nowPlayingStatus: http.StatusInternalServerError, + expectError: true, + errorContains: "failed to get current content", + }, + { + name: "invalid_preset_id", + presetID: 0, + nowPlayingResponse: ` + + + Test Track + + Test Track + PLAY_STATE +`, + nowPlayingStatus: http.StatusOK, + expectError: true, + errorContains: "preset ID must be between 1 and 6", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/now_playing": + if tt.nowPlayingStatus != 0 { + w.WriteHeader(tt.nowPlayingStatus) + } else { + w.WriteHeader(http.StatusOK) + } + if tt.nowPlayingResponse != "" { + w.Write([]byte(tt.nowPlayingResponse)) + } + case "/storePreset": + if tt.storePresetStatus != 0 { + w.WriteHeader(tt.storePresetStatus) + } else { + w.WriteHeader(http.StatusOK) + } + w.Write([]byte(``)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + err := client.StoreCurrentAsPreset(tt.presetID) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + } + }) + } +} + +func TestClient_StorePreset_XMLGeneration(t *testing.T) { + var capturedXML string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + r.Body.Read(body) + capturedXML = string(body) + w.Write([]byte(``)) + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + contentItem := &models.ContentItem{ + Source: "SPOTIFY", + Type: "uri", + Location: "spotify:playlist:37i9dQZF1DX0XUsuxWHRQd", + SourceAccount: "testuser", + IsPresetable: true, + ItemName: "Test Playlist", + ContainerArt: "https://example.com/art.jpg", + } + + err := client.StorePreset(3, contentItem) + if err != nil { + t.Fatalf("StorePreset failed: %v", err) + } + + // Verify XML structure + expectedElements := []string{ + `Test Playlist`, + `https://example.com/art.jpg`, + } + + for _, element := range expectedElements { + if !strings.Contains(capturedXML, element) { + t.Errorf("Expected XML to contain '%s', but got:\n%s", element, capturedXML) + } + } +} + +func TestClient_RemovePreset_XMLGeneration(t *testing.T) { + var capturedXML string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + r.Body.Read(body) + capturedXML = string(body) + w.Write([]byte(``)) + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + err := client.RemovePreset(4) + if err != nil { + t.Fatalf("RemovePreset failed: %v", err) + } + + // Verify XML structure - should only contain preset ID + expectedElements := []string{ + ``)) + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + err := client.StorePreset(1, scenario.contentItem) + if err != nil { + t.Errorf("Scenario '%s' failed: %s. Error: %v", scenario.name, scenario.description, err) + } + }) + } +} + +func TestClient_PresetTimestamps(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(``)) + })) + defer server.Close() + + client := &Client{ + baseURL: server.URL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } + + contentItem := &models.ContentItem{ + Source: "SPOTIFY", + Location: "spotify:track:test", + IsPresetable: true, + ItemName: "Test", + } + + startTime := time.Now().Unix() + + err := client.StorePreset(1, contentItem) + if err != nil { + t.Fatalf("StorePreset failed: %v", err) + } + + endTime := time.Now().Unix() + + // Timestamps should be set within the test timeframe + // This is a basic check - in a real scenario, we'd inspect the XML or server response + if endTime < startTime { + t.Error("Timestamps appear to be incorrect") + } +} diff --git a/pkg/models/websocket.go b/pkg/models/websocket.go index bd62251..49d9443 100644 --- a/pkg/models/websocket.go +++ b/pkg/models/websocket.go @@ -18,7 +18,7 @@ const ( // EventTypeConnectionState indicates a connection state change EventTypeConnectionState WebSocketEventType = "connectionStateUpdated" // EventTypePresetUpdated indicates a preset configuration change - EventTypePresetUpdated WebSocketEventType = "presetUpdated" + EventTypePresetUpdated WebSocketEventType = "presetsUpdated" // EventTypeZoneUpdated indicates a zone configuration change EventTypeZoneUpdated WebSocketEventType = "zoneUpdated" // EventTypeBassUpdated indicates a bass level change @@ -78,7 +78,7 @@ type WebSocketEvent struct { NowPlayingUpdated *NowPlayingUpdatedEvent `xml:"nowPlayingUpdated,omitempty"` VolumeUpdated *VolumeUpdatedEvent `xml:"volumeUpdated,omitempty"` ConnectionStateUpdated *ConnectionStateUpdatedEvent `xml:"connectionStateUpdated,omitempty"` - PresetUpdated *PresetUpdatedEvent `xml:"presetUpdated,omitempty"` + PresetUpdated *PresetUpdatedEvent `xml:"presetsUpdated,omitempty"` ZoneUpdated *ZoneUpdatedEvent `xml:"zoneUpdated,omitempty"` BassUpdated *BassUpdatedEvent `xml:"bassUpdated,omitempty"` ClockTimeUpdated *ClockTimeUpdatedEvent `xml:"clockTimeUpdated,omitempty"` @@ -195,9 +195,9 @@ func (cs *ConnectionState) GetSignalStrength() string { // PresetUpdatedEvent represents a preset update event type PresetUpdatedEvent struct { - XMLName xml.Name `xml:"presetUpdated"` + XMLName xml.Name `xml:"presetsUpdated"` DeviceID string `xml:"deviceID,attr"` - Preset Preset `xml:"preset"` + Presets Presets `xml:"presets"` } // ZoneUpdatedEvent represents a multiroom zone update event