mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
The ST10's /presets response after a factory reset emits self-closing <preset/> entries with no ContentItem child. cmd/soundtouch-cli's getPresets() handled the missing ContentItem in GetDisplayName() but then dereferenced preset.ContentItem.Source on the next line, panicking with "invalid memory address or nil pointer dereference" the moment the loop reached the first empty entry. A second placeholder shape was observed on healthy devices that were never reset: <preset id="0"><ContentItem source="INVALID_SOURCE" isPresetable="true"/></preset>. ContentItem is non-nil here, so the previous "ContentItem != nil" guard at other call sites still let these placeholders through into listings and into the AfterTouch datastore. Fix shape: pkg/models/presets.go - extend Preset.IsEmpty() to recognise both shapes (ContentItem == nil, OR Source == "" / "INVALID_SOURCE"). HasPresets, GetEmptyPresetSlots and GetUsedPresetSlots become honest about which slots actually carry playable content. cmd/soundtouch-cli/cmd_info.go (the crash site) - filter the slice via IsEmpty before the print loop, and switch the still-printed fields to the existing nil-safe Get* helpers. pkg/service/setup/setup.go - upgrade syncPresets's "ContentItem == nil" continue-guard to IsEmpty so Shape B placeholders don't get persisted in the AfterTouch datastore and then surface as junk rows in the admin web UI. cmd/soundtouch-cli/cmd_events.go, cmd/websocket-demo/main.go - same nil-guard upgrade. These already nil-checked so were crash-safe; the change is for consistency and to stop printing "Preset 0: (INVALID_SOURCE)" demo lines. examples/preset-management/main.go - had the same latent crash as cmd_info.go; same fix shape. Regression tests in pkg/models/presets_test.go cover both shapes using the exact XML observed in the wild: the reporter's three <preset/> placeholders plus the three INVALID_SOURCE entries from a live device. The reporter XML test walks every preset through the same accessor path the CLI used and asserts no panic. The soundtouch-web Go code does not deref preset.ContentItem.X anywhere - presets flow through as JSON - so no separate crash trap exists there. The web frontend will pick up the cleaner data once syncPresets stops persisting placeholders. Closes #308 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preset Management Example
This example demonstrates comprehensive preset management functionality for Bose SoundTouch devices.
Features Demonstrated
Core Preset Operations
- List Presets: View all configured presets with details
- Store Current Content: Save what's currently playing as a preset
- Store Specific Content: Save Spotify playlists, radio stations, etc.
- Select Presets: Choose and play a specific preset
- Remove Presets: Delete unwanted presets
- WebSocket Events: Monitor real-time preset updates
Content Types Supported
- Spotify: Playlists, albums, artists, tracks
- Radio Stations: TuneIn, local internet radio
- Local Music: NAS storage, local libraries
- Other Sources: Any presetable content source
Prerequisites
- Go 1.21+ installed on your system
- SoundTouch Device on your network
- Device IP Address (use discovery to find it)
Running the Example
1. Find Your Device IP
# From project root
go run ./cmd/soundtouch-cli discover devices
2. Run the Example
# Navigate to example directory
cd examples/preset-management
# Run with your device IP
go run . 192.168.1.100
What the Example Does
Step-by-Step Demonstration
- 📋 Current Presets: Lists all configured presets
- 🔍 Content Check: Analyzes what's currently playing
- 💾 Store Current: Saves current content as preset (if presetable)
- 💿 Store Spotify: Demonstrates storing a Spotify playlist
- 📻 Store Radio: Demonstrates storing a radio station
- 📋 Updated List: Shows presets after changes
- 🎯 Select Preset: Plays preset #1
- 📡 WebSocket Demo: Shows real-time preset events
Example Output
🎵 SoundTouch Preset Management Example
📱 Device: 192.168.1.100:8090
📋 Step 1: Getting current presets...
📻 Found 2 configured presets:
1. Morning Jazz
Source: SPOTIFY
Location: spotify:playlist:37i9dQZF1DXcBWIGoYBM5M
Created: 2024-01-15 08:30:00
2. K-LOVE Radio
Source: TUNEIN
Location: /v1/playback/station/s33828
Created: 2024-01-15 09:15:00
🆓 Available slots: [3 4 5 6]
🔍 Step 2: Checking current content...
🎵 Now Playing: Bohemian Rhapsody
Artist: Queen
Source: SPOTIFY
Presetable: true
Location: spotify:track:17GmwQ9Q3MTAz05OokmNNB
💾 Step 3: Storing current content as preset...
💾 Storing current content as preset 3...
✅ Successfully stored as preset 3
📡 Step 8: Demonstrating preset events...
📡 Connecting to WebSocket for real-time events...
✅ WebSocket connected, listening for preset events...
🔄 Making a preset change to trigger an event...
💾 Storing test preset 4 to trigger event...
⏳ Waiting 3 seconds for WebSocket event...
📡 Preset Update Event Received!
Device: A81B6A536A98
Presets count: 4
- Preset 1: Morning Jazz (SPOTIFY)
- Preset 2: K-LOVE Radio (TUNEIN)
- Preset 3: Bohemian Rhapsody (SPOTIFY)
- Preset 4: BBC Radio 1 (TUNEIN)
✅ Preset management demo completed!
Understanding the Code
Basic Preset Operations
// Get all presets
presets, err := client.GetPresets()
// Check if current content can be saved
presetable, err := client.IsCurrentContentPresetable()
// Store current content
err = client.StoreCurrentAsPreset(slotNumber)
// Store specific content
contentItem := &models.ContentItem{
Source: "SPOTIFY",
Type: "uri",
Location: "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M",
SourceAccount: "username",
IsPresetable: true,
ItemName: "Today's Top Hits",
}
err = client.StorePreset(slotNumber, contentItem)
// Select a preset
err = client.SelectPreset(1)
// Remove a preset
err = client.RemovePreset(6)
WebSocket Event Handling
// Create WebSocket client
wsClient := client.NewWebSocketClient(nil)
// Handle preset events
wsClient.OnPresetUpdated(func(event *models.PresetUpdatedEvent) {
fmt.Printf("Presets updated on device %s\n", event.DeviceID)
for _, preset := range event.Presets.Preset {
if !preset.IsEmpty() {
fmt.Printf("Preset %d: %s\n", preset.ID, preset.GetDisplayName())
}
}
})
// Connect and listen
err := wsClient.Connect()
defer wsClient.Close()
Content Location Examples
Spotify Content
// Playlist
Location: "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M"
// Album
Location: "spotify:album:4aawyAB9vmqN3uQ7FjRGTy"
// Artist
Location: "spotify:artist:6APm8EjxOHSYM5B4i3vT3q"
// Track
Location: "spotify:track:17GmwQ9Q3MTAz05OokmNNB"
Radio Stations
// TuneIn
Location: "/v1/playback/station/s33828"
// Internet Radio
Location: "https://stream.example.com/radio"
Getting Content Locations
Method 1: From Currently Playing
# Show current content details (includes location)
go run ./cmd/soundtouch-cli --host 192.168.1.100 play now
Method 2: From Spotify URLs
Convert Spotify web URLs to URIs:
- URL:
https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M - URI:
spotify:playlist:37i9dQZF1DXcBWIGoYBM5M
Error Scenarios
The example handles common error cases:
- No Content Playing: Gracefully handles empty now playing
- Non-Presetable Content: Shows when content can't be saved
- Full Preset Slots: Finds available slots or handles full device
- WebSocket Issues: Proper connection handling and cleanup
Integration with CLI
This example shows programmatic usage. For command-line usage:
# List presets
go run ./cmd/soundtouch-cli --host 192.168.1.100 preset list
# Store current content
go run ./cmd/soundtouch-cli --host 192.168.1.100 preset store-current --slot 1
# Store specific content
go run ./cmd/soundtouch-cli --host 192.168.1.100 preset store \
--slot 2 \
--source SPOTIFY \
--location "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M" \
--name "My Playlist"
# Select preset
go run ./cmd/soundtouch-cli --host 192.168.1.100 preset select --slot 1
# Remove preset
go run ./cmd/soundtouch-cli --host 192.168.1.100 preset remove --slot 6
Troubleshooting
Device Not Found
Error: Failed to connect to device: connection refused
Solution: Verify device IP and ensure device is powered on
Preset Store Failed
Error: Failed to store preset: content is not presetable
Solution: Not all content can be saved as presets (e.g., Bluetooth, some radio streams)
No Available Slots
Error: All preset slots are occupied
Solution: Remove an existing preset first or use a specific slot number
Related Documentation
- CLI Reference - Command-line usage
- Preset Implementation Guide - Technical details
- WebSocket Events - Real-time event handling
- API Reference - Complete API documentation
Use Cases
This example demonstrates patterns for:
- Smart Home Automation: Trigger presets based on time/events
- Music Management: Organize favorite content into quick-access presets
- Family Scenarios: Each person gets their own preset slots
- Party Mode: Pre-configure playlists for different moods
- Radio Favorites: Save frequently listened radio stations