mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Implement /now_playing and /sources endpoints with real device integration
## New Endpoints ### GET /now_playing ✅ - Rich XML models with PlayStatus, ShuffleSetting, RepeatSetting enums - Comprehensive playback information (track, artist, album, artwork, position) - Device capabilities (skip, seek, favorite functionality) - Smart display methods for different content types (music vs radio) - Duration formatting with position/total time display ### GET /sources ✅ - Complete audio source management with SourceStatus enum - Source categorization (Local/Remote, Streaming, Multiroom support) - Multiple account support (multiple Spotify accounts per device) - Availability filtering (Ready vs Unavailable sources) - Helper methods for quick capability checks ## Real Device Integration - Fetched actual XML responses from SoundTouch devices (192.168.178.28 & 192.168.178.35) - Updated all test fixtures with real device data (anonymized) - Enhanced XML models to handle all real-world fields and edge cases - Verified compatibility across different device types and configurations ## Enhanced CLI Tool - Added -nowplaying command with rich formatted output - Added -sources command with categorized source listing - Display enhancements: duration info, capabilities, source attributes - Improved build process to use ./build/ directory consistently ## Comprehensive Testing - 15+ unit tests for XML models with enum validation - Client integration tests with mock HTTP responses - Real device response validation - Edge case handling (empty states, network errors, invalid data) ## Documentation & Guidelines - Updated CLAUDE.md with build directory and real device testing guidelines - Enhanced README with comprehensive usage examples - Updated PLAN.md to reflect implementation progress - All examples use real device data patterns ## Quality Improvements - Type-safe XML unmarshaling with custom validation - Consistent error handling across all endpoints - Privacy protection (anonymized account information) - Production-ready code structure and patterns Features: ✅ GET /info - Device information ✅ GET /now_playing - Current playback status with full metadata ✅ GET /sources - Available audio sources with smart categorization ✅ UPnP device discovery ✅ Cross-platform CLI tool with rich output formatting ✅ Comprehensive test coverage with real device data ✅ Build automation with proper directory structure
This commit is contained in:
+250
-1
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/user_account/bose-soundtouch/pkg/client"
|
||||
"github.com/user_account/bose-soundtouch/pkg/config"
|
||||
"github.com/user_account/bose-soundtouch/pkg/discovery"
|
||||
"github.com/user_account/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -21,6 +22,8 @@ func main() {
|
||||
discover = flag.Bool("discover", false, "Discover SoundTouch devices via UPnP")
|
||||
discoverAll = flag.Bool("discover-all", false, "Discover all SoundTouch devices and show info")
|
||||
info = flag.Bool("info", false, "Get device information")
|
||||
nowPlaying = flag.Bool("nowplaying", false, "Get current playback status")
|
||||
sources = flag.Bool("sources", false, "Get available audio sources")
|
||||
help = flag.Bool("help", false, "Show help")
|
||||
)
|
||||
|
||||
@@ -32,7 +35,7 @@ func main() {
|
||||
}
|
||||
|
||||
// If no specific action is requested, show help
|
||||
if !*discover && !*discoverAll && !*info && *host == "" {
|
||||
if !*discover && !*discoverAll && !*info && !*nowPlaying && !*sources && *host == "" {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
@@ -55,6 +58,28 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle now playing
|
||||
if *nowPlaying {
|
||||
if *host == "" {
|
||||
log.Fatal("Host is required for nowplaying command. Use -host flag or -discover to find devices.")
|
||||
}
|
||||
if err := handleNowPlaying(*host, *port, *timeout); err != nil {
|
||||
log.Fatalf("Failed to get now playing: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle sources
|
||||
if *sources {
|
||||
if *host == "" {
|
||||
log.Fatal("Host is required for sources command. Use -host flag or -discover to find devices.")
|
||||
}
|
||||
if err := handleSources(*host, *port, *timeout); err != nil {
|
||||
log.Fatalf("Failed to get sources: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
@@ -70,12 +95,16 @@ func printHelp() {
|
||||
fmt.Println(" -discover Discover SoundTouch devices via UPnP")
|
||||
fmt.Println(" -discover-all Discover devices and show detailed info")
|
||||
fmt.Println(" -info Get device information (requires -host)")
|
||||
fmt.Println(" -nowplaying Get current playback status (requires -host)")
|
||||
fmt.Println(" -sources Get available audio sources (requires -host)")
|
||||
fmt.Println(" -help Show this help message")
|
||||
fmt.Println()
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" soundtouch-cli -discover")
|
||||
fmt.Println(" soundtouch-cli -discover-all")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -info")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -nowplaying")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -sources")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -port 8090 -info")
|
||||
}
|
||||
|
||||
@@ -220,3 +249,223 @@ func showDeviceInfoWithConfig(host string, port int, cfg *config.Config) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleNowPlaying(host string, port int, timeout time.Duration) error {
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
// Override config with command line arguments if provided
|
||||
if timeout > 0 {
|
||||
cfg.HTTPTimeout = timeout
|
||||
}
|
||||
|
||||
clientConfig := client.ClientConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Timeout: cfg.HTTPTimeout,
|
||||
UserAgent: cfg.UserAgent,
|
||||
}
|
||||
|
||||
soundtouchClient := client.NewClient(clientConfig)
|
||||
|
||||
fmt.Printf("Getting current playback status from %s:%d...\n", host, port)
|
||||
|
||||
// Get now playing info
|
||||
nowPlaying, err := soundtouchClient.GetNowPlaying()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get now playing: %w", err)
|
||||
}
|
||||
|
||||
// Display playback information
|
||||
fmt.Printf("Now Playing:\n")
|
||||
fmt.Printf(" Device ID: %s\n", nowPlaying.DeviceID)
|
||||
fmt.Printf(" Source: %s\n", nowPlaying.Source)
|
||||
fmt.Printf(" Status: %s\n", nowPlaying.PlayStatus.String())
|
||||
|
||||
if nowPlaying.IsEmpty() {
|
||||
fmt.Printf(" No content currently playing\n")
|
||||
} else {
|
||||
// Track information
|
||||
title := nowPlaying.GetDisplayTitle()
|
||||
artist := nowPlaying.GetDisplayArtist()
|
||||
|
||||
fmt.Printf(" Title: %s\n", title)
|
||||
if artist != "" {
|
||||
fmt.Printf(" Artist: %s\n", artist)
|
||||
}
|
||||
if nowPlaying.Album != "" {
|
||||
fmt.Printf(" Album: %s\n", nowPlaying.Album)
|
||||
}
|
||||
|
||||
// Radio/streaming info
|
||||
if nowPlaying.IsRadio() && nowPlaying.StationName != "" {
|
||||
fmt.Printf(" Station: %s\n", nowPlaying.StationName)
|
||||
}
|
||||
|
||||
// Duration/Position info
|
||||
if nowPlaying.HasTimeInfo() {
|
||||
if duration := nowPlaying.FormatDuration(); duration != "" {
|
||||
fmt.Printf(" Duration: %s\n", duration)
|
||||
} else if position := nowPlaying.FormatPosition(); position != "" {
|
||||
fmt.Printf(" Position: %s\n", position)
|
||||
}
|
||||
}
|
||||
|
||||
// Playback settings
|
||||
if nowPlaying.ShuffleSetting != "" {
|
||||
fmt.Printf(" Shuffle: %s\n", nowPlaying.ShuffleSetting.String())
|
||||
}
|
||||
if nowPlaying.RepeatSetting != "" {
|
||||
fmt.Printf(" Repeat: %s\n", nowPlaying.RepeatSetting.String())
|
||||
}
|
||||
|
||||
// Artwork
|
||||
if artURL := nowPlaying.GetArtworkURL(); artURL != "" {
|
||||
fmt.Printf(" Artwork: %s\n", artURL)
|
||||
}
|
||||
|
||||
// Additional metadata
|
||||
if nowPlaying.Description != "" {
|
||||
fmt.Printf(" Description: %s\n", nowPlaying.Description)
|
||||
}
|
||||
if nowPlaying.StationLocation != "" {
|
||||
fmt.Printf(" Station Location: %s\n", nowPlaying.StationLocation)
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
var capabilities []string
|
||||
if nowPlaying.CanSkip() {
|
||||
capabilities = append(capabilities, "Skip")
|
||||
}
|
||||
if nowPlaying.CanSkipPrevious() {
|
||||
capabilities = append(capabilities, "Skip Previous")
|
||||
}
|
||||
if nowPlaying.IsSeekSupported() {
|
||||
capabilities = append(capabilities, "Seek")
|
||||
}
|
||||
if nowPlaying.CanFavorite() {
|
||||
capabilities = append(capabilities, "Favorite")
|
||||
}
|
||||
if len(capabilities) > 0 {
|
||||
fmt.Printf(" Capabilities: %s\n", strings.Join(capabilities, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSources(host string, port int, timeout time.Duration) error {
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
// Override config with command line arguments if provided
|
||||
if timeout > 0 {
|
||||
cfg.HTTPTimeout = timeout
|
||||
}
|
||||
|
||||
clientConfig := client.ClientConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Timeout: cfg.HTTPTimeout,
|
||||
UserAgent: cfg.UserAgent,
|
||||
}
|
||||
|
||||
soundtouchClient := client.NewClient(clientConfig)
|
||||
|
||||
fmt.Printf("Getting available audio sources from %s:%d...\n", host, port)
|
||||
|
||||
// Get sources info
|
||||
sources, err := soundtouchClient.GetSources()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get sources: %w", err)
|
||||
}
|
||||
|
||||
// Display sources information
|
||||
fmt.Printf("Audio Sources:\n")
|
||||
fmt.Printf(" Device ID: %s\n", sources.DeviceID)
|
||||
fmt.Printf(" Total Sources: %d\n", sources.GetSourceCount())
|
||||
fmt.Printf(" Ready Sources: %d\n", sources.GetReadySourceCount())
|
||||
fmt.Println()
|
||||
|
||||
// Display available sources
|
||||
availableSources := sources.GetAvailableSources()
|
||||
if len(availableSources) > 0 {
|
||||
fmt.Printf("Ready Sources:\n")
|
||||
for _, source := range availableSources {
|
||||
fmt.Printf(" • %s", source.GetDisplayName())
|
||||
if source.SourceAccount != "" && source.SourceAccount != source.Source {
|
||||
fmt.Printf(" (%s)", source.SourceAccount)
|
||||
}
|
||||
|
||||
var attributes []string
|
||||
if source.IsLocalSource() {
|
||||
attributes = append(attributes, "Local")
|
||||
} else {
|
||||
attributes = append(attributes, "Remote")
|
||||
}
|
||||
if source.SupportsMultiroom() {
|
||||
attributes = append(attributes, "Multiroom")
|
||||
}
|
||||
if source.IsStreamingService() {
|
||||
attributes = append(attributes, "Streaming")
|
||||
}
|
||||
|
||||
if len(attributes) > 0 {
|
||||
fmt.Printf(" [%s]", strings.Join(attributes, ", "))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Display unavailable sources
|
||||
var unavailableSources []models.SourceItem
|
||||
for _, source := range sources.SourceItem {
|
||||
if source.Status.IsUnavailable() {
|
||||
unavailableSources = append(unavailableSources, source)
|
||||
}
|
||||
}
|
||||
|
||||
if len(unavailableSources) > 0 {
|
||||
fmt.Printf("Unavailable Sources:\n")
|
||||
for _, source := range unavailableSources {
|
||||
fmt.Printf(" • %s", source.GetDisplayName())
|
||||
if source.SourceAccount != "" && source.SourceAccount != source.Source {
|
||||
fmt.Printf(" (%s)", source.SourceAccount)
|
||||
}
|
||||
fmt.Printf(" [%s]", source.Status.String())
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Summary by category
|
||||
fmt.Printf("Categories:\n")
|
||||
if sources.HasSpotify() {
|
||||
spotifySources := sources.GetReadySpotifySources()
|
||||
fmt.Printf(" Spotify: %d account(s) ready\n", len(spotifySources))
|
||||
}
|
||||
if sources.HasBluetooth() {
|
||||
fmt.Printf(" Bluetooth: Ready\n")
|
||||
}
|
||||
if sources.HasAux() {
|
||||
fmt.Printf(" AUX Input: Ready\n")
|
||||
}
|
||||
|
||||
streamingSources := sources.GetStreamingSources()
|
||||
readyStreaming := 0
|
||||
for _, source := range streamingSources {
|
||||
if source.Status.IsReady() {
|
||||
readyStreaming++
|
||||
}
|
||||
}
|
||||
if readyStreaming > 0 {
|
||||
fmt.Printf(" Streaming Services: %d ready\n", readyStreaming)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user