feat: implement POST /key endpoint for media controls with host:port parsing

Major Features:
• POST /key endpoint implementation with XML model and validation
• Comprehensive media control commands (play, pause, stop, volume, presets)
• Automatic host:port parsing in CLI for improved UX
• Production-ready with full test coverage

Key Control Implementation:
• Add Key model with XML marshaling and validation (pkg/models/key.go)
• Support all standard keys: PLAY, PAUSE, STOP, PREV_TRACK, NEXT_TRACK, VOLUME_UP/DOWN, PRESET_1-6
• Client methods: SendKey(), Play(), Pause(), Stop(), VolumeUp(), VolumeDown(), SelectPreset()
• CLI commands: -play, -pause, -stop, -next, -prev, -volume-up, -volume-down, -preset, -key
• Critical fix: Use 'Gabbo' as sender (only accepted value by SoundTouch API)

Host:Port Parsing Enhancement:
• Support -host 192.168.178.28:8090 format in addition to separate -host/-port flags
• Robust parsing with IPv4, IPv6, and hostname support
• Graceful fallback for invalid input
• Backward compatible with existing usage

Testing & Documentation:
• Comprehensive unit tests for key functionality and host:port parsing
• Integration tested with real SoundTouch 10 and SoundTouch 20 devices
• Complete documentation in docs/KEY-CONTROLS.md and docs/HOST-PORT-PARSING.md
• All tests pass, no diagnostics errors

Breaking Changes: None
Backward Compatibility: Fully maintained

Tested with:
• SoundTouch 10 (192.168.178.28:8090) 
• SoundTouch 20 (192.168.178.35:8090) 
This commit is contained in:
Tobias Gesellchen
2026-01-08 23:46:34 +01:00
parent de2ff3550f
commit 7d73da7986
7 changed files with 1354 additions and 9 deletions
+211
View File
@@ -0,0 +1,211 @@
# Host:Port Parsing Feature
This document describes the automatic host:port parsing functionality added to the SoundTouch CLI, which allows users to specify both host and port in a single `-host` flag.
## Overview
The SoundTouch CLI now supports parsing host and port combinations in the `-host` flag, making it more user-friendly and following common CLI patterns. Users can specify either just a host (using the default or `-port` flag) or a complete `host:port` combination.
## Usage Examples
### Basic Host:Port Format
```bash
# Specify host and port together
soundtouch-cli -host 192.168.1.100:8090 -info
soundtouch-cli -host 192.168.1.35:8090 -play
soundtouch-cli -host soundtouch.local:8090 -pause
```
### Traditional Separate Flags (Still Supported)
```bash
# Traditional separate host and port flags
soundtouch-cli -host 192.168.1.100 -port 8090 -info
soundtouch-cli -host 192.168.1.35 -port 8090 -play
```
### Precedence Rules
When both formats are used, the port specified in the host:port format takes precedence:
```bash
# Uses port 8090 from host:port, ignores -port 9999
soundtouch-cli -host 192.168.1.100:8090 -port 9999 -info
```
## Supported Formats
### IPv4 Addresses
```bash
# Standard IPv4 with port
soundtouch-cli -host 192.168.1.100:8090 -info
# IPv4 without port (uses default 8090)
soundtouch-cli -host 192.168.1.100 -info
```
### Hostnames
```bash
# Hostname with port
soundtouch-cli -host soundtouch.local:8090 -info
soundtouch-cli -host bose-kitchen:9000 -play
# Hostname without port (uses default)
soundtouch-cli -host soundtouch.local -info
```
### IPv6 Addresses
```bash
# IPv6 with port (requires brackets)
soundtouch-cli -host [::1]:8090 -info
soundtouch-cli -host [2001:db8::1]:8090 -play
# IPv6 without port
soundtouch-cli -host ::1 -info
```
## Implementation Details
### Parsing Function
The `parseHostPort()` function handles the parsing logic:
```go
func parseHostPort(hostPort string, defaultPort int) (string, int)
```
### Parsing Rules
1. **Contains colon**: Attempts to split using `net.SplitHostPort()`
2. **Valid port**: Port must be numeric and in range 1-65535
3. **Invalid port**: Falls back to original host and default port
4. **No colon**: Returns original input as host with default port
5. **Parse error**: Returns original input as host with default port
### Error Handling
The parser is designed to be forgiving and always return usable values:
- **Invalid port numbers**: Fall back to default port
- **Malformed input**: Return original input as host
- **Empty input**: Handle gracefully
- **Multiple colons**: Handled by `net.SplitHostPort()` error handling
## Test Coverage
### Unit Tests
Comprehensive test coverage in `cmd/soundtouch-cli/main_test.go`:
- ✅ IPv4 addresses with and without ports
- ✅ Hostnames with and without ports
- ✅ IPv6 addresses with and without ports
- ✅ Invalid port handling
- ✅ Edge cases (empty strings, malformed input)
- ✅ Real-world SoundTouch scenarios
### Integration Tests
Tested with real SoundTouch devices:
- ✅ SoundTouch 10 (192.168.1.100:8090)
- ✅ SoundTouch 20 (192.168.1.35:8090)
## Benefits
### User Experience
- **Simplified syntax**: `host:port` is more intuitive than separate flags
- **Consistent with other tools**: Follows common CLI patterns
- **Backward compatible**: Existing scripts continue to work
- **Copy-paste friendly**: Can copy host:port from discovery output
### Development Benefits
- **Robust parsing**: Handles edge cases gracefully
- **Comprehensive tests**: Well-tested functionality
- **Clean implementation**: Uses Go standard library
- **Error resilience**: Falls back to sensible defaults
## Examples with Real Devices
### Discovery + Direct Usage
```bash
# Discover devices to find host:port
$ soundtouch-cli -discover
Found SoundTouch devices:
My SoundTouch Device (192.168.1.100:8090) - SoundTouch 20
# Use discovered host:port directly
$ soundtouch-cli -host 192.168.1.100:8090 -play
```
### Different Port Scenarios
```bash
# Standard SoundTouch port
soundtouch-cli -host 192.168.1.100:8090 -info
# Custom port (if device configured differently)
soundtouch-cli -host 192.168.1.100:9000 -info
# Default port fallback
soundtouch-cli -host 192.168.1.100 -info # Uses 8090
```
### Error Scenarios
```bash
# Invalid port - uses default 8090
soundtouch-cli -host 192.168.1.100:invalid -info
# Out of range port - uses default 8090
soundtouch-cli -host 192.168.1.100:99999 -info
# Malformed input - treats as hostname
soundtouch-cli -host "malformed::input" -info
```
## CLI Help Output
The help text has been updated to reflect the new functionality:
```
Options:
-host <ip> SoundTouch device IP address (or host:port)
-port <port> SoundTouch device port (default: 8090)
Examples:
soundtouch-cli -host 192.168.1.100 -info
soundtouch-cli -host 192.168.1.100:8090 -info
soundtouch-cli -host 192.168.1.100:8090 -pause
soundtouch-cli -host 192.168.1.100:8090 -preset 1
```
## Technical Implementation
### Function Signature
```go
// parseHostPort splits a host:port string into separate host and port components
// If no port is specified, returns the original host and the provided default port
func parseHostPort(hostPort string, defaultPort int) (string, int)
```
### Key Features
- Uses Go's `net.SplitHostPort()` for robust parsing
- Validates port range (1-65535)
- Handles IPv6 addresses correctly with brackets
- Graceful fallback for all error conditions
- Preserves original host for malformed input
### Integration Points
The parsed values are used throughout the CLI:
- Device info commands
- Now playing queries
- Source management
- Key control commands
- All API endpoint interactions
## Future Enhancements
Potential improvements for the future:
1. **URL Format Support**: Support full URLs like `http://192.168.1.100:8090`
2. **Service Discovery**: Auto-detect port via service discovery protocols
3. **Configuration File**: Save frequently used host:port combinations
4. **Environment Variables**: Support `SOUNDTOUCH_HOST` with host:port format
5. **Validation**: More sophisticated host validation (DNS lookup, ping)
## Reference
- **Implementation**: `cmd/soundtouch-cli/main.go` (parseHostPort function)
- **Tests**: `cmd/soundtouch-cli/main_test.go`
- **Go Documentation**: `net.SplitHostPort()` for parsing logic
- **Standards**: Follows RFC 3986 for host:port format
+256
View File
@@ -0,0 +1,256 @@
# Key Control Implementation
This document describes the implementation of the POST `/key` endpoint for media control commands in the Bose SoundTouch API client.
## Overview
The key control functionality allows sending media control commands to SoundTouch devices, including play/pause, volume adjustment, track navigation, and preset selection.
## Implementation Files
- `pkg/models/key.go` - XML model and constants for key commands
- `pkg/models/key_test.go` - Comprehensive tests for key functionality
- `pkg/client/client.go` - Client methods for sending key commands
- `cmd/soundtouch-cli/main.go` - CLI commands for key controls
## API Specification
### POST /key
Sends a key command to the SoundTouch device.
**Request Format:**
```xml
<key state="press" sender="Gabbo">KEY_NAME</key>
```
**Response Format:**
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<status>/key</status>
```
### Important Discovery: Sender Field
During implementation, we discovered that the `sender` attribute is critical for successful key commands. Only specific sender values are accepted:
-**"Gabbo"** - Works (canonical example from official documentation)
- ❌ "GoClient" - Rejected with CLIENT_XML_ERROR (1019)
- ❌ "SoundTouch app" - Rejected with CLIENT_XML_ERROR (1019)
- ❌ "" (empty) - Rejected with CLIENT_XML_ERROR (1019)
Our implementation uses **"Gabbo"** as the default sender, which is the standard value used in official SoundTouch examples.
## Available Key Commands
### Media Controls
- `PLAY` - Start playback
- `PAUSE` - Pause playback
- `STOP` - Stop playback
- `PREV_TRACK` - Previous track
- `NEXT_TRACK` - Next track
### Volume Controls
- `VOLUME_UP` - Increase volume
- `VOLUME_DOWN` - Decrease volume
### Presets
- `PRESET_1` through `PRESET_6` - Select preset 1-6
## Client API
### Basic Methods
```go
// Send any valid key command
err := client.SendKey(models.KeyPlay)
// Send key press (default behavior)
err := client.SendKeyPress(models.KeyPlay)
// Send key release
err := client.SendKeyRelease(models.KeyPlay)
```
### Convenience Methods
```go
// Media controls
err := client.Play()
err := client.Pause()
err := client.Stop()
err := client.NextTrack()
err := client.PrevTrack()
// Volume controls
err := client.VolumeUp()
err := client.VolumeDown()
// Preset selection (1-6)
err := client.SelectPreset(1)
```
### Key Validation
```go
// Check if a key value is valid
isValid := models.IsValidKey("PLAY") // true
isValid := models.IsValidKey("INVALID") // false
// Get all valid key values
allKeys := models.GetAllValidKeys()
```
## CLI Usage
### Individual Key Commands
```bash
# Media controls
soundtouch-cli -host 192.168.1.100 -play
soundtouch-cli -host 192.168.1.100 -pause
soundtouch-cli -host 192.168.1.100 -stop
soundtouch-cli -host 192.168.1.100 -next
soundtouch-cli -host 192.168.1.100 -prev
# Volume controls
soundtouch-cli -host 192.168.1.100 -volume-up
soundtouch-cli -host 192.168.1.100 -volume-down
# Preset selection
soundtouch-cli -host 192.168.1.100 -preset 1
soundtouch-cli -host 192.168.1.100 -preset 6
```
### Generic Key Command
```bash
# Send any valid key using the -key flag
soundtouch-cli -host 192.168.1.100 -key PLAY
soundtouch-cli -host 192.168.1.100 -key STOP
soundtouch-cli -host 192.168.1.100 -key PRESET_3
```
### Error Handling
```bash
# Invalid key validation
$ soundtouch-cli -host 192.168.1.100 -key INVALID
Failed to send key command: invalid key value: INVALID
# Multiple commands rejected
$ soundtouch-cli -host 192.168.1.100 -play -pause
Failed to send key command: only one key command can be sent at a time
# Missing host
$ soundtouch-cli -play
Host is required for key commands. Use -host flag or -discover to find devices.
```
## Testing
### Unit Tests
The implementation includes comprehensive unit tests in `pkg/models/key_test.go`:
- XML marshaling/unmarshaling
- Key validation
- Constructor functions
- Constants validation
- Benchmark tests
Run tests:
```bash
go test ./pkg/models/...
```
### Integration Testing
Tested with real SoundTouch devices:
- **SoundTouch 10** (192.168.1.100:8090) ✅
- **SoundTouch 20** (192.168.1.35:8090) ✅
All key commands successfully sent and executed on both devices.
## Code Examples
### Basic Usage
```go
package main
import (
"log"
"github.com/user_account/bose-soundtouch/pkg/client"
"github.com/user_account/bose-soundtouch/pkg/models"
)
func main() {
// Create client
soundtouchClient := client.NewClientFromHost("192.168.1.100")
// Play music
if err := soundtouchClient.Play(); err != nil {
log.Fatalf("Failed to play: %v", err)
}
// Adjust volume
if err := soundtouchClient.VolumeUp(); err != nil {
log.Fatalf("Failed to increase volume: %v", err)
}
// Select preset
if err := soundtouchClient.SelectPreset(1); err != nil {
log.Fatalf("Failed to select preset: %v", err)
}
}
```
### Advanced Usage with Validation
```go
func sendKeyCommand(client *client.Client, keyValue string) error {
// Validate before sending
if !models.IsValidKey(keyValue) {
return fmt.Errorf("invalid key: %s", keyValue)
}
return client.SendKey(keyValue)
}
func sendAllValidKeys(client *client.Client) {
for _, key := range models.GetAllValidKeys() {
fmt.Printf("Sending key: %s\n", key)
if err := client.SendKey(key); err != nil {
log.Printf("Failed to send %s: %v", key, err)
}
time.Sleep(1 * time.Second) // Avoid overwhelming the device
}
}
```
## Implementation Notes
1. **Sender Field Critical**: The `sender` attribute must be "Gabbo" for commands to be accepted
2. **XML Format**: Simple XML structure without namespaces or headers
3. **State Handling**: Both "press" and "release" states are supported
4. **Input Validation**: All key values are validated before sending to the device
5. **Error Handling**: Comprehensive error handling for invalid keys and API errors
6. **CLI Safety**: Only one key command allowed per CLI invocation to prevent conflicts
## Future Enhancements
Potential areas for future development:
1. **Key Sequences**: Support for sending multiple key commands in sequence
2. **Macros**: Predefined key command sequences (e.g., "power on and play preset 1")
3. **Key Hold**: Support for key hold duration for volume changes
4. **Device State**: Check device state before sending commands
5. **Async Commands**: Non-blocking key command execution
6. **Key Mapping**: Custom key mappings for different device types
## Reference
- **Official API**: Based on Bose SoundTouch Web API documentation
- **Test Devices**: Validated with SoundTouch 10 and SoundTouch 20
- **Standards**: Follows existing project patterns and conventions