diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index 4968d7b..1cfd5a4 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -52,7 +52,7 @@ func main() { sources = flag.Bool("sources", false, "Get available audio sources") name = flag.Bool("name", false, "Get device name") capabilities = flag.Bool("capabilities", false, "Get device capabilities") - presets = flag.Bool("presets", false, "Get configured presets") + presets = flag.Bool("presets", false, "Get configured presets (requires -host)") key = flag.String("key", "", "Send key command (PLAY, PAUSE, STOP, PREV_TRACK, NEXT_TRACK, THUMBS_UP, THUMBS_DOWN, BOOKMARK, POWER, MUTE, VOLUME_UP, VOLUME_DOWN, PRESET_1-6, AUX_INPUT, SHUFFLE_OFF, SHUFFLE_ON, REPEAT_OFF, REPEAT_ONE, REPEAT_ALL)") play = flag.Bool("play", false, "Send PLAY key command") pause = flag.Bool("pause", false, "Send PAUSE key command") diff --git a/docs/API-Endpoints-Overview.md b/docs/API-Endpoints-Overview.md index ea02a46..150a88a 100644 --- a/docs/API-Endpoints-Overview.md +++ b/docs/API-Endpoints-Overview.md @@ -201,17 +201,15 @@ Retrieves the configured presets. ``` -### POST /presets 🔄 **Planned** +### POST /presets ❌ **Not Supported** Creates or updates a preset. -**Request XML:** -```xml - - - Preset Name - - -``` +**Status**: According to the official Bose SoundTouch API documentation, POST operations on `/presets` are marked as "N/A" - this endpoint officially does not support preset creation or modification via API. + +**Alternative Methods**: +- Use the official Bose SoundTouch mobile app +- Use physical preset buttons on the device (long-press while content is playing) +- Changes made via these methods will be visible through the GET endpoint ## Advanced Features diff --git a/docs/PRESET-MANAGEMENT.md b/docs/PRESET-MANAGEMENT.md new file mode 100644 index 0000000..8362945 --- /dev/null +++ b/docs/PRESET-MANAGEMENT.md @@ -0,0 +1,352 @@ +# Preset Management - Bose SoundTouch API + +This document covers preset management functionality in the Bose SoundTouch API client. + +## Overview + +Bose SoundTouch devices support up to 6 presets that can store favorite music sources, playlists, radio stations, and other audio content. The API provides comprehensive **read access** to preset information, while **write access** (creating/updating presets) is officially not supported by the API. + +## Current Implementation Status + +### ✅ **Fully Supported - Read Operations** +- `GET /presets` - Retrieve all configured presets +- Comprehensive preset analysis and helper methods +- Integration with CLI tool for viewing presets + +### ❌ **Not Supported - Write Operations** +- `POST /presets` - Officially marked as "N/A" in Bose API documentation +- Preset clearing/deletion via API +- Direct preset creation from currently playing content + +### ✅ **Supported Alternatives** +- Use official Bose SoundTouch mobile app (iOS/Android) +- Use physical preset buttons on the device (long-press while playing content) +- Changes made via these methods are visible through the API's GET endpoint + +## Reading Presets + +### CLI Usage + +```bash +# Get all configured presets +soundtouch-cli -host 192.168.1.10 -presets +``` + +**Example Output:** +``` +Configured Presets: + Used Slots: 6/6 + Spotify Presets: 6 + +Preset 1: My Favorite Playlist + Source: SPOTIFY (user@example.com) + Type: tracklisturl + Created: 2024-04-30 07:37:40 + Updated: 2024-04-30 07:37:40 + Artwork: https://i.scdn.co/image/... + +Preset 2: Rock Hits Radio + Source: TUNEIN + Type: station + Artwork: https://cdn-radiotime-logos.tunein.com/... + +Available Slots: [] +Most Recent: Preset 1 (My Favorite Playlist) +``` + +### Go Library Usage + +```go +package main + +import ( + "fmt" + "github.com/user_account/bose-soundtouch/pkg/client" +) + +func main() { + // Create client + soundtouchClient := client.NewClientFromHost("192.168.1.10") + + // Get all presets + presets, err := soundtouchClient.GetPresets() + if err != nil { + panic(err) + } + + // Display preset information + fmt.Printf("Total presets: %d\n", presets.GetPresetCount()) + fmt.Printf("Used slots: %d\n", len(presets.GetUsedPresetSlots())) + fmt.Printf("Empty slots: %v\n", presets.GetEmptyPresetSlots()) + + // Check for Spotify presets + spotifyPresets := presets.GetSpotifyPresets() + fmt.Printf("Spotify presets: %d\n", len(spotifyPresets)) + + // Get specific preset + preset1 := presets.GetPresetByID(1) + if preset1 != nil && !preset1.IsEmpty() { + fmt.Printf("Preset 1: %s\n", preset1.GetDisplayName()) + fmt.Printf(" Source: %s\n", preset1.GetSource()) + fmt.Printf(" Type: %s\n", preset1.GetContentType()) + + if preset1.HasTimestamps() { + fmt.Printf(" Created: %s\n", preset1.GetCreatedTime()) + fmt.Printf(" Updated: %s\n", preset1.GetUpdatedTime()) + } + } + + // Find most recent preset + if recent := presets.GetMostRecentPreset(); recent != nil { + fmt.Printf("Most recent: Preset %d (%s)\n", + recent.ID, recent.GetDisplayName()) + } +} +``` + +## Preset Data Structure + +### XML Response Format +```xml + + + + My Favorite Songs + https://i.scdn.co/image/... + + + + + Classic Rock Radio + https://cdn-radiotime-logos.tunein.com/... + + + +``` + +### Go Model +```go +type Presets struct { + XMLName xml.Name `xml:"presets"` + Preset []Preset `xml:"preset"` +} + +type Preset struct { + XMLName xml.Name `xml:"preset"` + ID int `xml:"id,attr"` + CreatedOn *int64 `xml:"createdOn,attr,omitempty"` + UpdatedOn *int64 `xml:"updatedOn,attr,omitempty"` + ContentItem *ContentItem `xml:"ContentItem,omitempty"` +} +``` + +## Helper Methods + +### Preset Analysis +```go +// Get preset by ID +preset := presets.GetPresetByID(3) + +// Check if preset has content +if !preset.IsEmpty() { + // Use preset +} + +// Get presets by source type +spotifyPresets := presets.GetPresetsBySource("SPOTIFY") +tuneInPresets := presets.GetPresetsBySource("TUNEIN") + +// Find available slots +emptySlots := presets.GetEmptyPresetSlots() // Returns [4, 5] if slots 4-5 are empty +usedSlots := presets.GetUsedPresetSlots() // Returns [1, 2, 3, 6] if those are used +``` + +### Preset Content Analysis +```go +// Get display information +name := preset.GetDisplayName() // "My Playlist" or "Preset 1" fallback +source := preset.GetSource() // "SPOTIFY", "TUNEIN", etc. +account := preset.GetSourceAccount() // "user@example.com" +contentType := preset.GetContentType() // "playlist", "station", etc. +artwork := preset.GetArtworkURL() // Album/station artwork URL + +// Check preset characteristics +isSpotify := preset.IsSpotifyPreset() +isPresetable := preset.IsPresetable() + +// Time information (if available) +if preset.HasTimestamps() { + created := preset.GetCreatedTime() + updated := preset.GetUpdatedTime() +} +``` + +### Content Type Examples + +Common content types found in presets: + +| Source | Type | Description | Example Location | +|--------|------|-------------|------------------| +| `SPOTIFY` | `tracklisturl` | Playlist/Album | `/playback/container/c3Bv...` | +| `SPOTIFY` | `track` | Single Track | `/playback/container/c3Bv...` | +| `TUNEIN` | `station` | Radio Station | `s12345` | +| `PANDORA` | `station` | Pandora Station | `TR:station:12345` | +| `AMAZON` | `playlist` | Amazon Playlist | `amzn1.dv.gti...` | + +## Preset Selection + +While you cannot create presets via API, you can select existing presets: + +### Via Key Commands +```bash +# Select preset 1-6 using key commands +soundtouch-cli -host 192.168.1.10 -preset 1 +soundtouch-cli -host 192.168.1.10 -key PRESET_3 +``` + +### Via Go Library +```go +// Select preset using key command +err := soundtouchClient.SelectPreset(1) + +// Or use direct key command +err := soundtouchClient.SendKey("PRESET_1") +``` + +## Limitations and Workarounds + +### API Design Limitations +1. **No API-based preset creation** - `POST /presets` is officially marked as "N/A" in Bose documentation +2. **No preset deletion** - Cannot clear preset slots via API (by design) +3. **No preset modification** - Cannot update existing preset content via API (by design) +4. **Read-only access** - API intentionally provides comprehensive read access only + +### Working Alternatives + +#### 1. Official Bose SoundTouch App +- iOS/Android app allows full preset management +- Can create, update, and delete presets +- Changes sync automatically with device + +#### 2. Physical Device Controls +- Use preset buttons (1-6) on the device +- Long-press while content is playing to save as preset +- Short-press to select saved preset + +#### 3. Web Interface (if available) +- Some devices may have a web interface +- Access via `http://device-ip:8090` in browser +- May provide preset management controls + +## Checking Presetability + +Before attempting to save content as a preset (via app/hardware), you can check if the current content supports preset saving: + +```go +// Check if currently playing content can be saved as preset +nowPlaying, err := client.GetNowPlaying() +if err != nil { + return err +} + +if nowPlaying.ContentItem != nil && nowPlaying.ContentItem.IsPresetable { + fmt.Println("✓ Current content can be saved as a preset") + fmt.Printf(" Content: %s\n", nowPlaying.ContentItem.ItemName) + fmt.Printf(" Source: %s\n", nowPlaying.ContentItem.Source) + fmt.Printf(" Type: %s\n", nowPlaying.ContentItem.Type) +} else { + fmt.Println("✗ Current content cannot be saved as a preset") +} +``` + +Or use the convenience method: +```go +// Simple presetability check +presetable, err := client.IsCurrentContentPresetable() +if err != nil { + return err +} + +if presetable { + fmt.Println("✓ Content is presetable - use app or device buttons to save") +} else { + fmt.Println("✗ Content cannot be saved as preset") +} +``` + +## Best Practices + +### 1. Check Available Slots +```go +presets, err := client.GetPresets() +if err != nil { + return err +} + +emptySlots := presets.GetEmptyPresetSlots() +if len(emptySlots) == 0 { + fmt.Println("All preset slots are occupied") + // Consider which preset to overwrite +} else { + fmt.Printf("Available preset slots: %v\n", emptySlots) +} +``` + +### 2. Analyze Current Presets +```go +// Get summary statistics +summary := presets.GetPresetsSummary() +fmt.Printf("Total: %d, Used: %d, Empty: %d\n", + summary["total"], summary["used"], summary["empty"]) + +// Check source distribution +if summary["SPOTIFY"] > 0 { + fmt.Printf("Spotify presets: %d\n", summary["SPOTIFY"]) +} +if summary["TUNEIN"] > 0 { + fmt.Printf("TuneIn presets: %d\n", summary["TUNEIN"]) +} +``` + +### 3. Handle Preset History +```go +// Find recently used presets +if recent := presets.GetMostRecentPreset(); recent != nil { + fmt.Printf("Most recently updated: Preset %d (%s)\n", + recent.ID, recent.GetDisplayName()) +} + +if oldest := presets.GetOldestPreset(); oldest != nil { + fmt.Printf("Oldest preset: Preset %d (%s)\n", + oldest.ID, oldest.GetDisplayName()) +} +``` + +## Future Development + +### API Design Decision +Based on the official Bose SoundTouch API documentation, preset creation via API is intentionally not supported. This is likely a design decision to: +1. **Maintain user control** - Presets are personal configurations best managed by the user +2. **Prevent accidental overrides** - Avoid third-party apps accidentally modifying user presets +3. **Ensure UI consistency** - Keep preset management in official interfaces +4. **Security considerations** - Limit configuration changes to authenticated official apps + +### No Further Investigation Needed +The preset creation limitation is **not a bug or missing feature** - it's the intended API design. The comprehensive read access provides everything needed for applications to work with existing user configurations. + +## Related Documentation + +- [API Endpoints Overview](API-Endpoints-Overview.md) - Complete API reference +- [Volume Controls](VOLUME-CONTROLS.md) - Volume management +- [Key Controls](KEY-CONTROLS.md) - Media control commands +- [Source Selection](SOURCE-SELECTION.md) - Audio source management + +## Summary + +Preset management in the Bose SoundTouch API is **intentionally read-only** by design. The API provides excellent capabilities for analyzing and understanding preset configurations, but preset creation must be done through official channels (app or device). This is a deliberate design decision that respects user control over their personal preset configurations. + +For most use cases, reading preset information is sufficient for building applications that work with existing user configurations. For preset creation, guide users to use the official app or device controls, which provide the proper user experience and validation. \ No newline at end of file diff --git a/docs/STATUS.md b/docs/STATUS.md index 87eeb33..31656b0 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -62,9 +62,6 @@ This project implements a comprehensive Go client library and CLI tool for Bose ## 🔄 Next Priority (Remaining Endpoints) -### **Control Endpoints - HIGH PRIORITY** -- `POST /presets` - Create/update presets - ### **System Endpoints - MEDIUM PRIORITY** - `GET /clockTime`, `POST /clockTime` - Device time @@ -84,6 +81,7 @@ This project implements a comprehensive Go client library and CLI tool for Bose | **Control Endpoints** | 5/5 | 5 | 100% | | **System Endpoints** | 3/8 | 8 | 37.5% | | **Real-time Features** | 0/1 | 1 | 0% | +| **Preset Management** | 1/1 | 1 | 100% | | **Overall Progress** | 14/20 | 20 | **70%** | ## 🏆 Major Accomplishments @@ -111,6 +109,7 @@ This project implements a comprehensive Go client library and CLI tool for Bose - **Source Selection**: Full source switching with convenience methods (-spotify, -bluetooth, -aux) - **Bass Control**: Complete bass management with validation and convenience methods - **Balance Control**: Stereo balance adjustment with left/right channel control +- **Preset Management**: Complete preset analysis with helper methods (read-only by API design) - **API Compliance**: Proper press+release key pattern implementation - **Safety First**: Volume warnings and limits for user protection - **User Experience**: Host:port parsing (e.g., `-host 192.168.1.100:8090`) @@ -146,6 +145,7 @@ This project implements a comprehensive Go client library and CLI tool for Bose - `docs/API-Endpoints-Overview.md` - API reference with status ✅ - `docs/KEY-CONTROLS.md` - Media control implementation ✅ - `docs/VOLUME-CONTROLS.md` - Volume management guide ✅ +- `docs/PRESET-MANAGEMENT.md` - Preset analysis and limitations ✅ - `docs/HOST-PORT-PARSING.md` - Enhanced CLI feature ✅ - `docs/PLAN.md` - Development roadmap (updated) ✅ - `docs/PROJECT-PATTERNS.md` - Development guidelines ✅ @@ -173,10 +173,10 @@ This project implements a comprehensive Go client library and CLI tool for Bose ## 🎯 Current Focus Areas ### Immediate Next Steps (1-2 Sessions) -1. **Clock/Time Management** - `GET/POST /clockTime` and `/clockDisplay` endpoints +1. **Remaining System Endpoints** - Device reboot, additional diagnostics ### Short Term (3-5 Sessions) -4. **System Endpoints** - Clock, network info, balance +4. **Preset Creation Research** - Investigate alternative approaches for preset writing 5. **Error Enhancement** - More detailed error responses 6. **CLI Polish** - Additional convenience features @@ -189,13 +189,14 @@ This project implements a comprehensive Go client library and CLI tool for Bose ### ✅ Production Ready Features - **Core Device Control**: Information, media controls, volume +- **Audio Management**: Complete bass and balance control +- **Preset Management**: Complete preset analysis (API is read-only by design) - **Safety Features**: Volume warnings, input validation - **Error Handling**: Comprehensive error messages - **Cross-Platform**: Works on all major platforms - **Real Device Tested**: Validated hardware integration ### 🔄 Areas for Enhancement -- Additional control endpoints (source, bass, presets) - WebSocket real-time events - Web interface - Advanced multiroom features @@ -217,9 +218,8 @@ This project implements a comprehensive Go client library and CLI tool for Bose - [ ] Real-time event streaming - [ ] Web application interface -## 📝 Notes - ### Recent Major Updates +- **2026-01-09**: Preset management (read-only) with comprehensive analysis methods - **2026-01-09**: Balance control implementation completing audio management trilogy - **2026-01-09**: Bass control implementation with range validation and convenience methods - **2026-01-09**: Source selection implementation with convenience methods @@ -237,6 +237,9 @@ This project implements a comprehensive Go client library and CLI tool for Bose - Some devices may have slight API variations - mDNS discovery may fail in corporate networks (expected behavior) +### API Design Decisions +- Preset creation is intentionally not supported via API (official documentation: POST /presets = "N/A") + ### Development Notes - All major architectural decisions documented - Code follows Go best practices @@ -245,5 +248,5 @@ This project implements a comprehensive Go client library and CLI tool for Bose --- -**Status**: 🟢 **Healthy Development** - Audio controls complete (70% overall), ready for system endpoints -**Next Session Focus**: Clock and time management endpoints \ No newline at end of file +**Status**: 🟢 **Healthy Development** - Audio controls and preset management complete (70% overall) +**Next Session Focus**: WebSocket real-time events or remaining system endpoints \ No newline at end of file diff --git a/pkg/client/client.go b/pkg/client/client.go index 0d38704..87002d7 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -126,6 +126,36 @@ func (c *Client) GetPresets() (*models.Presets, error) { return &presets, nil } +// GetNextAvailablePresetSlot returns the next available preset slot (1-6), or error if all are used +func (c *Client) GetNextAvailablePresetSlot() (int, error) { + presets, err := c.GetPresets() + if err != nil { + return 0, fmt.Errorf("failed to get presets: %w", err) + } + + emptySlots := presets.GetEmptyPresetSlots() + if len(emptySlots) == 0 { + return 0, fmt.Errorf("all preset slots are occupied") + } + + // Return the first available slot + return emptySlots[0], nil +} + +// IsCurrentContentPresetable checks if the currently playing content can be saved as a preset +func (c *Client) IsCurrentContentPresetable() (bool, error) { + nowPlaying, err := c.GetNowPlaying() + if err != nil { + return false, fmt.Errorf("failed to get now playing: %w", err) + } + + if nowPlaying.IsEmpty() || nowPlaying.ContentItem == nil { + return false, nil + } + + return nowPlaying.ContentItem.IsPresetable, 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/models/presets.go b/pkg/models/presets.go index 7a25e66..bf9e951 100644 --- a/pkg/models/presets.go +++ b/pkg/models/presets.go @@ -14,6 +14,7 @@ type Presets struct { // Preset represents an individual preset type Preset struct { + XMLName xml.Name `xml:"preset"` ID int `xml:"id,attr"` CreatedOn *int64 `xml:"createdOn,attr,omitempty"` UpdatedOn *int64 `xml:"updatedOn,attr,omitempty"`