feat: implement comprehensive content selection with streamUrl format support

 New Features:
- Add SelectContentItem() method for direct ContentItem selection
- Add SelectLocalInternetRadio() with full streamUrl format support
- Add SelectLocalMusic() for SoundTouch App Media Server content
- Add SelectStoredMusic() for UPnP/DLNA media server content

📻 streamUrl Format Support:
- Full implementation of wiki specification for LOCAL_INTERNET_RADIO
- Support for proxy URLs: http://contentapi.gmuth.de/station.php?name=Station&streamUrl=ActualStream
- Direct stream URL support for simple internet radio
- Complete ContentItem structure with metadata and artwork

🖥️ CLI Commands:
- Add 'source internet-radio' command with streamUrl support
- Add 'source local-music' command for local media server content
- Add 'source stored-music' command for UPnP/DLNA content
- Add 'source content' command for advanced generic selection
- All commands include comprehensive flag support and validation

🧪 Testing:
- Add 17+ comprehensive unit tests covering all scenarios
- Test streamUrl format validation and parsing
- Test error handling and parameter validation
- Test default value assignment and ContentItem construction
- All tests passing with full coverage

📚 Documentation:
- Update CLI-REFERENCE.md with new command examples
- Add complete content-selection example with working code
- Add implementation summary document
- Include API documentation for all new methods
- Add usage examples for both API and CLI

🔗 References:
Implements features from SoundTouch WebServices API Wiki:
- https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_internet_radio---streamurl-format
- https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_music

🎯 Benefits:
- Complete API coverage for advanced content selection
- Backward compatible with existing code
- Flexible design with both convenience and power-user methods
- Production-ready with comprehensive testing and documentation

Co-authored-by: SoundTouch WebServices API Wiki <https://github.com/thlucas1/homeassistantcomponent_soundtouchplus>
This commit is contained in:
Tobias Gesellchen
2026-02-02 16:44:25 +01:00
co-authored by lnx01
parent 7ec4ee67af
commit 0d5746a6a5
8 changed files with 1527 additions and 3 deletions
+180
View File
@@ -0,0 +1,180 @@
# Content Selection Example
This example demonstrates the advanced content selection features of the Bose SoundTouch Go client, including support for LOCAL_INTERNET_RADIO with streamUrl format, LOCAL_MUSIC, and STORED_MUSIC content.
## Features Demonstrated
### 1. LOCAL_INTERNET_RADIO with streamUrl Format
- Uses proxy server format: `http://contentapi.gmuth.de/station.php?name=StationName&streamUrl=ActualStreamURL`
- Supports complex radio station metadata
- Artwork and station information
### 2. LOCAL_INTERNET_RADIO Direct Streams
- Direct HTTP/HTTPS stream URLs
- Simple internet radio playback
- MP3 and other audio format support
### 3. LOCAL_MUSIC Content
- SoundTouch App Media Server content
- Albums, tracks, artists, playlists
- Requires local SoundTouch Media Server running
### 4. STORED_MUSIC Content
- UPnP/DLNA media server content
- NAS libraries and Windows Media Player sharing
- Network-attached storage music libraries
### 5. Generic ContentItem Selection
- Direct ContentItem object creation
- Maximum flexibility for any content type
- All SoundTouch sources supported
## Prerequisites
- SoundTouch device on your network
- Device IP address
- Go 1.21+ installed
### Optional (for specific examples):
- **LOCAL_MUSIC**: SoundTouch App Media Server running on a computer
- **STORED_MUSIC**: UPnP/DLNA media server (Windows Media Player, NAS, etc.)
## Usage
```bash
# Build and run
go run main.go <device_ip>
# Example
go run main.go 192.168.1.100
```
## Example Output
```
🎵 SoundTouch Content Selection Example
📱 Device: 192.168.1.100:8090
📻 Step 1: Demonstrating LOCAL_INTERNET_RADIO with streamUrl format...
📡 Using streamUrl format with proxy server...
Station: Antenne Chillout
Proxy URL: http://contentapi.gmuth.de/station.php?name=Antenne%20Chillout&streamUrl=https://stream.antenne.de/chillout/stream/aacp
✅ Successfully selected internet radio with streamUrl format
🎵 Now Playing:
Title: Antenne Chillout
Source: LOCAL_INTERNET_RADIO
Status: Playing
Location: http://contentapi.gmuth.de/station.php?name=Antenne%20Chillout&streamUrl=https://stream.antenne.de/chillout/stream/aacp
📻 Step 2: Demonstrating LOCAL_INTERNET_RADIO with direct stream...
📡 Using direct stream URL...
Stream: Test Audio Stream
URL: https://freetestdata.com/wp-content/uploads/2021/09/Free_Test_Data_1MB_MP3.mp3
✅ Successfully selected direct internet radio stream
💿 Step 3: Demonstrating LOCAL_MUSIC selection...
⚠️ LOCAL_MUSIC demo failed (this requires SoundTouch App Media Server): failed to select local music: HTTP 404 Not Found
💾 Step 4: Demonstrating STORED_MUSIC selection...
⚠️ STORED_MUSIC demo failed (this requires UPnP/DLNA media server): failed to select stored music: HTTP 404 Not Found
🎯 Step 5: Demonstrating generic ContentItem selection...
🎯 Using generic ContentItem selection...
Content: K-LOVE Radio
Source: TUNEIN
Location: /v1/playbook/station/s33828
✅ Successfully selected content using ContentItem
✅ Content selection demo completed!
```
## API Methods Demonstrated
### SelectLocalInternetRadio
```go
err := client.SelectLocalInternetRadio(location, sourceAccount, itemName, containerArt)
```
### SelectLocalMusic
```go
err := client.SelectLocalMusic(location, sourceAccount, itemName, containerArt)
```
### SelectStoredMusic
```go
err := client.SelectStoredMusic(location, sourceAccount, itemName, containerArt)
```
### SelectContentItem (Advanced)
```go
contentItem := &models.ContentItem{
Source: "LOCAL_INTERNET_RADIO",
Type: "stationurl",
Location: "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio",
SourceAccount: "",
IsPresetable: true,
ItemName: "My Radio Station",
ContainerArt: "https://example.com/art.png",
}
err := client.SelectContentItem(contentItem)
```
## CLI Usage Examples
These API methods are also available via the CLI:
```bash
# Internet radio with streamUrl format
soundtouch-cli --host 192.168.1.100 source internet-radio \
--location "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio" \
--name "My Station" \
--artwork "https://example.com/art.png"
# Local music content
soundtouch-cli --host 192.168.1.100 source local-music \
--location "album:983" \
--account "3f205110-4a57-4e91-810a-123456789012" \
--name "Welcome to the New"
# Stored music content
soundtouch-cli --host 192.168.1.100 source stored-music \
--location "6_a2874b5d_4f83d999" \
--account "d09708a1-5953-44bc-a413-123456789012/0" \
--name "Christmas Album"
# Generic content selection (advanced)
soundtouch-cli --host 192.168.1.100 source content \
--source LOCAL_INTERNET_RADIO \
--location "https://stream.example.com/radio" \
--name "My Stream" \
--type stationurl \
--presetable
```
## Implementation Notes
### streamUrl Format
The streamUrl format uses a proxy server that accepts the actual stream URL as a parameter. This allows for:
- Complex metadata handling
- Stream URL obfuscation
- Cross-origin request handling
- Additional processing capabilities
### ContentItem Structure
All content selection methods create a `ContentItem` with appropriate defaults:
- `Type` is automatically set based on source
- `IsPresetable` defaults to true
- `ItemName` gets a sensible default if not provided
### Error Handling
The example gracefully handles missing services:
- LOCAL_MUSIC requires SoundTouch App Media Server
- STORED_MUSIC requires UPnP/DLNA media server
- Some internet streams may be geo-restricted
## Related Documentation
- [SoundTouch WebServices API Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
- [CLI Reference](../../docs/CLI-REFERENCE.md)
- [Navigation Guide](../../docs/NAVIGATION-GUIDE.md)
+274
View File
@@ -0,0 +1,274 @@
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/gesellix/bose-soundtouch/pkg/client"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
// Get device IP from command line
deviceIP := os.Args[1]
// Create client
config := &client.Config{
Host: deviceIP,
Port: 8090,
Timeout: 10 * time.Second,
}
c := client.NewClient(config)
fmt.Printf("🎵 SoundTouch Content Selection Example\n")
fmt.Printf("📱 Device: %s:%d\n\n", config.Host, config.Port)
// Demonstrate various content selection methods
if err := demonstrateContentSelection(c); err != nil {
log.Fatalf("Demo failed: %v", err)
}
fmt.Println("\n✅ Content selection demo completed!")
}
func demonstrateContentSelection(c *client.Client) error {
// 1. Demonstrate LOCAL_INTERNET_RADIO with streamUrl format
fmt.Println("📻 Step 1: Demonstrating LOCAL_INTERNET_RADIO with streamUrl format...")
if err := demoLocalInternetRadioStreamUrl(c); err != nil {
return fmt.Errorf("failed LOCAL_INTERNET_RADIO demo: %w", err)
}
// Wait and show what's playing
time.Sleep(3 * time.Second)
if err := showNowPlaying(c); err != nil {
fmt.Printf("⚠️ Could not get now playing: %v\n", err)
}
// 2. Demonstrate LOCAL_INTERNET_RADIO with direct stream
fmt.Println("\n📻 Step 2: Demonstrating LOCAL_INTERNET_RADIO with direct stream...")
if err := demoLocalInternetRadioDirect(c); err != nil {
return fmt.Errorf("failed direct stream demo: %w", err)
}
// Wait and show what's playing
time.Sleep(3 * time.Second)
if err := showNowPlaying(c); err != nil {
fmt.Printf("⚠️ Could not get now playing: %v\n", err)
}
// 3. Demonstrate LOCAL_MUSIC selection
fmt.Println("\n💿 Step 3: Demonstrating LOCAL_MUSIC selection...")
if err := demoLocalMusic(c); err != nil {
fmt.Printf("⚠️ LOCAL_MUSIC demo failed (this requires SoundTouch App Media Server): %v\n", err)
} else {
// Wait and show what's playing
time.Sleep(3 * time.Second)
if err := showNowPlaying(c); err != nil {
fmt.Printf("⚠️ Could not get now playing: %v\n", err)
}
}
// 4. Demonstrate STORED_MUSIC selection
fmt.Println("\n💾 Step 4: Demonstrating STORED_MUSIC selection...")
if err := demoStoredMusic(c); err != nil {
fmt.Printf("⚠️ STORED_MUSIC demo failed (this requires UPnP/DLNA media server): %v\n", err)
} else {
// Wait and show what's playing
time.Sleep(3 * time.Second)
if err := showNowPlaying(c); err != nil {
fmt.Printf("⚠️ Could not get now playing: %v\n", err)
}
}
// 5. Demonstrate generic ContentItem selection
fmt.Println("\n🎯 Step 5: Demonstrating generic ContentItem selection...")
if err := demoGenericContentItem(c); err != nil {
return fmt.Errorf("failed generic ContentItem demo: %w", err)
}
// Wait and show what's playing
time.Sleep(3 * time.Second)
if err := showNowPlaying(c); err != nil {
fmt.Printf("⚠️ Could not get now playing: %v\n", err)
}
return nil
}
func demoLocalInternetRadioStreamUrl(c *client.Client) error {
fmt.Printf(" 📡 Using streamUrl format with proxy server...\n")
// Example using the streamUrl format from the wiki
// This uses a proxy server that accepts the actual stream URL as a parameter
location := "http://contentapi.gmuth.de/station.php?name=Antenne%20Chillout&streamUrl=https://stream.antenne.de/chillout/stream/aacp"
itemName := "Antenne Chillout"
containerArt := "https://www.radio.net/300/antennechillout.png?version=7fddbc7d3f37557ad3291d66fff40f323e1779d6"
fmt.Printf(" Station: %s\n", itemName)
fmt.Printf(" Proxy URL: %s\n", location)
err := c.SelectLocalInternetRadio(location, "", itemName, containerArt)
if err != nil {
return err
}
fmt.Printf(" ✅ Successfully selected internet radio with streamUrl format\n")
return nil
}
func demoLocalInternetRadioDirect(c *client.Client) error {
fmt.Printf(" 📡 Using direct stream URL...\n")
// Example using a direct stream URL
location := "https://freetestdata.com/wp-content/uploads/2021/09/Free_Test_Data_1MB_MP3.mp3"
itemName := "Test Audio Stream"
fmt.Printf(" Stream: %s\n", itemName)
fmt.Printf(" URL: %s\n", location)
err := c.SelectLocalInternetRadio(location, "", itemName, "")
if err != nil {
return err
}
fmt.Printf(" ✅ Successfully selected direct internet radio stream\n")
return nil
}
func demoLocalMusic(c *client.Client) error {
fmt.Printf(" 💿 Selecting LOCAL_MUSIC content...\n")
// Example LOCAL_MUSIC selection (requires SoundTouch App Media Server)
// These are example values - in practice, you'd get these from navigation
location := "album:983"
sourceAccount := "3f205110-4a57-4e91-810a-123456789012" // Example GUID
itemName := "Welcome to the New"
containerArt := "http://192.168.1.14:8085/v1/albums/983/image"
fmt.Printf(" Album: %s\n", itemName)
fmt.Printf(" Location: %s\n", location)
fmt.Printf(" Account: %s\n", sourceAccount)
err := c.SelectLocalMusic(location, sourceAccount, itemName, containerArt)
if err != nil {
return err
}
fmt.Printf(" ✅ Successfully selected local music content\n")
return nil
}
func demoStoredMusic(c *client.Client) error {
fmt.Printf(" 💾 Selecting STORED_MUSIC content...\n")
// Example STORED_MUSIC selection (requires UPnP/DLNA media server)
// These are example values - in practice, you'd get these from navigation
location := "6_a2874b5d_4f83d999"
sourceAccount := "d09708a1-5953-44bc-a413-123456789012/0" // Example UPnP server GUID
itemName := "Christmas Album"
fmt.Printf(" Album: %s\n", itemName)
fmt.Printf(" Location: %s\n", location)
fmt.Printf(" Account: %s\n", sourceAccount)
err := c.SelectStoredMusic(location, sourceAccount, itemName, "")
if err != nil {
return err
}
fmt.Printf(" ✅ Successfully selected stored music content\n")
return nil
}
func demoGenericContentItem(c *client.Client) error {
fmt.Printf(" 🎯 Using generic ContentItem selection...\n")
// Example using SelectContentItem directly for maximum flexibility
contentItem := &models.ContentItem{
Source: "TUNEIN",
Type: "stationurl",
Location: "/v1/playbook/station/s33828", // K-LOVE Radio
SourceAccount: "",
IsPresetable: true,
ItemName: "K-LOVE Radio",
ContainerArt: "http://cdn-profiles.tunein.com/s33828/images/logog.png",
}
fmt.Printf(" Content: %s\n", contentItem.ItemName)
fmt.Printf(" Source: %s\n", contentItem.Source)
fmt.Printf(" Location: %s\n", contentItem.Location)
err := c.SelectContentItem(contentItem)
if err != nil {
return err
}
fmt.Printf(" ✅ Successfully selected content using ContentItem\n")
return nil
}
func showNowPlaying(c *client.Client) error {
nowPlaying, err := c.GetNowPlaying()
if err != nil {
return err
}
if nowPlaying.IsEmpty() {
fmt.Printf(" ⏸️ No content currently playing\n")
return nil
}
fmt.Printf(" 🎵 Now Playing:\n")
fmt.Printf(" Title: %s\n", nowPlaying.GetDisplayTitle())
if nowPlaying.GetDisplayArtist() != "" {
fmt.Printf(" Artist: %s\n", nowPlaying.GetDisplayArtist())
}
if nowPlaying.Album != "" {
fmt.Printf(" Album: %s\n", nowPlaying.Album)
}
fmt.Printf(" Source: %s\n", nowPlaying.Source)
fmt.Printf(" Status: %s\n", nowPlaying.PlayStatus.String())
if nowPlaying.ContentItem != nil && nowPlaying.ContentItem.Location != "" {
fmt.Printf(" Location: %s\n", nowPlaying.ContentItem.Location)
}
return nil
}
func printUsage() {
fmt.Println("🎵 SoundTouch Content Selection Example")
fmt.Println()
fmt.Println("This example demonstrates the new content selection features:")
fmt.Println("• LOCAL_INTERNET_RADIO with streamUrl format")
fmt.Println("• LOCAL_INTERNET_RADIO with direct stream URLs")
fmt.Println("• LOCAL_MUSIC content selection")
fmt.Println("• STORED_MUSIC content selection")
fmt.Println("• Generic ContentItem selection")
fmt.Println()
fmt.Println("Usage:")
fmt.Printf(" %s <device_ip>\n", os.Args[0])
fmt.Println()
fmt.Println("Example:")
fmt.Printf(" %s 192.168.1.100\n", os.Args[0])
fmt.Println()
fmt.Println("Prerequisites:")
fmt.Println("• SoundTouch device on your network")
fmt.Println("• Device IP address")
fmt.Println("• Device powered on and connected")
fmt.Println()
fmt.Println("Note:")
fmt.Println("• LOCAL_MUSIC examples require SoundTouch App Media Server")
fmt.Println("• STORED_MUSIC examples require UPnP/DLNA media server")
fmt.Println("• Some streams may not work depending on your network/location")
}