diff --git a/cmd/soundtouch-cli/cmd_discover.go b/cmd/soundtouch-cli/cmd_discover.go new file mode 100644 index 0000000..690b337 --- /dev/null +++ b/cmd/soundtouch-cli/cmd_discover.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/user_account/bose-soundtouch/pkg/config" + "github.com/user_account/bose-soundtouch/pkg/discovery" + "github.com/urfave/cli/v2" +) + +// discoverDevices handles device discovery command +func discoverDevices(c *cli.Context) error { + timeout := c.Duration("timeout") + showAll := c.Bool("all") + + fmt.Printf("Discovering SoundTouch devices...\n") + if showAll { + fmt.Printf("Timeout: %v\n", timeout) + fmt.Printf("Mode: Detailed information\n") + } + fmt.Println() + + // Load configuration + cfg, err := config.LoadFromEnv() + if err != nil { + cfg = config.DefaultConfig() + } + + // Override discovery timeout if provided + if timeout > 0 { + cfg.DiscoveryTimeout = timeout + } + + // Create discovery service + discoveryService := discovery.NewUnifiedDiscoveryService(cfg) + ctx, cancel := context.WithTimeout(context.Background(), cfg.DiscoveryTimeout+5*time.Second) + defer cancel() + + // Perform discovery + devices, err := discoveryService.DiscoverDevices(ctx) + if err != nil { + return fmt.Errorf("discovery failed: %w", err) + } + + if len(devices) == 0 { + fmt.Println("No SoundTouch devices found on the network.") + fmt.Println() + fmt.Println("This could mean:") + fmt.Println("- No SoundTouch devices are powered on") + fmt.Println("- Devices are on a different network segment") + fmt.Println("- Network blocks multicast traffic") + fmt.Println("- Firewall is blocking discovery ports") + return nil + } + + // Display results + fmt.Printf("Found %d SoundTouch device(s):\n\n", len(devices)) + + for i, device := range devices { + fmt.Printf("%d. %s\n", i+1, device.Name) + fmt.Printf(" Host: %s:%d\n", device.Host, device.Port) + fmt.Printf(" Model: %s\n", device.ModelID) + + if device.SerialNo != "" { + fmt.Printf(" Serial: %s\n", device.SerialNo) + } + + if device.Location != "" { + fmt.Printf(" Location: %s\n", device.Location) + } + + if showAll { + fmt.Printf(" Last Seen: %s\n", device.LastSeen.Format("2006-01-02 15:04:05")) + } + + // Add spacing between devices + if i < len(devices)-1 { + fmt.Println() + } + } + + fmt.Println() + fmt.Printf("Use any of these hosts with other commands:\n") + fmt.Printf("Example: soundtouch-cli info --host %s\n", devices[0].Host) + + return nil +} diff --git a/cmd/soundtouch-cli/cmd_info.go b/cmd/soundtouch-cli/cmd_info.go new file mode 100644 index 0000000..287371b --- /dev/null +++ b/cmd/soundtouch-cli/cmd_info.go @@ -0,0 +1,191 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// getDeviceInfo handles the device info command +func getDeviceInfo(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting device information", clientConfig.Host, clientConfig.Port) + + deviceInfo, err := client.GetDeviceInfo() + if err != nil { + return fmt.Errorf("failed to get device info: %w", err) + } + + // Display basic device information + fmt.Printf("Device Information:\n") + fmt.Printf(" Name: %s\n", deviceInfo.Name) + fmt.Printf(" Type: %s\n", deviceInfo.Type) + fmt.Printf(" Device ID: %s\n", deviceInfo.DeviceID) + if deviceInfo.MargeAccountUUID != "" { + fmt.Printf(" Account UUID: %s\n", deviceInfo.MargeAccountUUID) + } + + if len(deviceInfo.NetworkInfo) > 0 { + fmt.Printf(" Network Info:\n") + + for _, net := range deviceInfo.NetworkInfo { + fmt.Printf(" - Type: %s\n", net.Type) + fmt.Printf(" MAC Address: %s\n", net.MacAddress) + fmt.Printf(" IP Address: %s\n", net.IPAddress) + } + } + + if len(deviceInfo.Components) > 0 { + fmt.Printf(" Components:\n") + + for _, component := range deviceInfo.Components { + fmt.Printf(" - Category: %s\n", component.ComponentCategory) + if component.SoftwareVersion != "" { + fmt.Printf(" Software Version: %s\n", component.SoftwareVersion) + } + if component.SerialNumber != "" { + fmt.Printf(" Serial Number: %s\n", component.SerialNumber) + } + } + } + + return nil +} + +// getDeviceName handles getting the device name +func getDeviceName(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting device name", clientConfig.Host, clientConfig.Port) + + name, err := client.GetName() + if err != nil { + return fmt.Errorf("failed to get device name: %w", err) + } + + fmt.Printf("Device Name: %s\n", name) + return nil +} + +// setDeviceName handles setting the device name +func setDeviceName(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + newName := c.String("value") + if newName == "" { + return fmt.Errorf("device name cannot be empty") + } + + PrintDeviceHeader(fmt.Sprintf("Setting device name to '%s'", newName), clientConfig.Host, clientConfig.Port) + + err = client.SetName(newName) + if err != nil { + return fmt.Errorf("failed to set device name: %w", err) + } + + PrintSuccess(fmt.Sprintf("Device name set to '%s'", newName)) + return nil +} + +// getCapabilities handles getting device capabilities +func getCapabilities(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting device capabilities", clientConfig.Host, clientConfig.Port) + + capabilities, err := client.GetCapabilities() + if err != nil { + return fmt.Errorf("failed to get capabilities: %w", err) + } + + fmt.Printf("Device Capabilities:\n") + fmt.Printf(" Device ID: %s\n", capabilities.DeviceID) + + // Network capabilities + networkCaps := capabilities.GetNetworkCapabilities() + if len(networkCaps) > 0 { + fmt.Printf(" Network Capabilities:\n") + + for _, cap := range networkCaps { + fmt.Printf(" - %s\n", cap) + } + } + + // Extended capabilities + capNames := capabilities.GetCapabilityNames() + if len(capNames) > 0 { + fmt.Printf(" Extended Capabilities:\n") + + for _, capName := range capNames { + capability := capabilities.GetCapabilityByName(capName) + fmt.Printf(" - %s", capName) + if capability.URL != "" { + fmt.Printf(" (%s)", capability.URL) + } + fmt.Println() + } + } + + return nil +} + +// getPresets handles getting device presets +func getPresets(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting device presets", clientConfig.Host, clientConfig.Port) + + presets, err := client.GetPresets() + if err != nil { + return fmt.Errorf("failed to get presets: %w", err) + } + + fmt.Printf("Device Presets:\n") + + if len(presets.Preset) == 0 { + fmt.Printf(" No presets configured\n") + return nil + } + + fmt.Printf(" Configured Presets:\n") + for _, preset := range presets.Preset { + fmt.Printf(" %d. %s\n", preset.ID, preset.GetDisplayName()) + fmt.Printf(" Source: %s\n", preset.ContentItem.Source) + + if preset.ContentItem.SourceAccount != "" && preset.ContentItem.SourceAccount != preset.ContentItem.Source { + fmt.Printf(" Account: %s\n", preset.ContentItem.SourceAccount) + } + + if preset.ContentItem.Location != "" { + fmt.Printf(" Location: %s\n", preset.ContentItem.Location) + } + + // Show preset creation time if available + if preset.CreatedOn != nil && *preset.CreatedOn != 0 { + fmt.Printf(" Created: Unix timestamp %d\n", *preset.CreatedOn) + } + } + + return nil +} diff --git a/cmd/soundtouch-cli/cmd_playback.go b/cmd/soundtouch-cli/cmd_playback.go new file mode 100644 index 0000000..fd2af5f --- /dev/null +++ b/cmd/soundtouch-cli/cmd_playback.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + + "github.com/user_account/bose-soundtouch/pkg/models" + "github.com/urfave/cli/v2" +) + +// getNowPlaying handles getting the current playback status +func getNowPlaying(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting current playback status", clientConfig.Host, clientConfig.Port) + + nowPlaying, err := client.GetNowPlaying() + if err != nil { + return fmt.Errorf("failed to get now playing: %w", err) + } + + fmt.Printf("Now Playing:\n") + fmt.Printf(" Device ID: %s\n", nowPlaying.DeviceID) + + if nowPlaying.IsEmpty() { + fmt.Printf(" Status: No content playing\n") + return nil + } + + fmt.Printf(" Source: %s\n", nowPlaying.Source) + fmt.Printf(" Status: %s\n", nowPlaying.PlayStatus.String()) + + if nowPlaying.Track != "" { + fmt.Printf(" Track: %s\n", nowPlaying.Track) + } + + if nowPlaying.Artist != "" { + fmt.Printf(" Artist: %s\n", nowPlaying.Artist) + } + + if nowPlaying.Album != "" { + fmt.Printf(" Album: %s\n", nowPlaying.Album) + } + + if nowPlaying.HasTimeInfo() { + fmt.Printf(" Duration: %s\n", nowPlaying.FormatDuration()) + if nowPlaying.Position != nil { + fmt.Printf(" Position: %s\n", nowPlaying.FormatPosition()) + } + } + + if nowPlaying.StreamType != "" { + fmt.Printf(" Stream Type: %s\n", nowPlaying.StreamType) + } + + if nowPlaying.PlayStatus == models.PlayStatusBuffering { + fmt.Printf(" Note: Content is buffering\n") + } + + return nil +} + +// playCommand handles play command +func playCommand(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Sending play command", clientConfig.Host, clientConfig.Port) + + err = client.SendKeyPressOnly(models.KeyPlay) + if err != nil { + return fmt.Errorf("failed to send play command: %w", err) + } + + PrintSuccess("Play command sent") + return nil +} + +// pauseCommand handles pause command +func pauseCommand(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Sending pause command", clientConfig.Host, clientConfig.Port) + + err = client.SendKeyPressOnly(models.KeyPause) + if err != nil { + return fmt.Errorf("failed to send pause command: %w", err) + } + + PrintSuccess("Pause command sent") + return nil +} + +// stopCommand handles stop command +func stopCommand(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Sending stop command", clientConfig.Host, clientConfig.Port) + + err = client.SendKeyPressOnly(models.KeyStop) + if err != nil { + return fmt.Errorf("failed to send stop command: %w", err) + } + + PrintSuccess("Stop command sent") + return nil +} + +// nextCommand handles next track command +func nextCommand(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Sending next track command", clientConfig.Host, clientConfig.Port) + + err = client.SendKeyPressOnly(models.KeyNextTrack) + if err != nil { + return fmt.Errorf("failed to send next track command: %w", err) + } + + PrintSuccess("Next track command sent") + return nil +} + +// prevCommand handles previous track command +func prevCommand(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Sending previous track command", clientConfig.Host, clientConfig.Port) + + err = client.SendKeyPressOnly(models.KeyPrevTrack) + if err != nil { + return fmt.Errorf("failed to send previous track command: %w", err) + } + + PrintSuccess("Previous track command sent") + return nil +} diff --git a/cmd/soundtouch-cli/cmd_source.go b/cmd/soundtouch-cli/cmd_source.go new file mode 100644 index 0000000..5e42750 --- /dev/null +++ b/cmd/soundtouch-cli/cmd_source.go @@ -0,0 +1,165 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/urfave/cli/v2" +) + +// listSources handles listing available audio sources +func listSources(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting available sources", clientConfig.Host, clientConfig.Port) + + sources, err := client.GetSources() + if err != nil { + return fmt.Errorf("failed to get sources: %w", err) + } + + fmt.Printf("Available Audio Sources:\n") + fmt.Printf(" Device ID: %s\n", sources.DeviceID) + + // Show ready sources first + 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") + } + if source.IsLocalSource() { + attributes = append(attributes, "Available") + } + if len(attributes) > 0 { + fmt.Printf(" [%s]", strings.Join(attributes, ", ")) + } + fmt.Println() + } + } + + // Show all configured sources + fmt.Printf(" All Sources:\n") + for _, source := range sources.SourceItem { + status := "Available" + if !source.IsLocalSource() { + status = "Remote" + } + + fmt.Printf(" • %s (%s)\n", source.GetDisplayName(), status) + if source.SourceAccount != "" && source.SourceAccount != source.Source { + fmt.Printf(" Account: %s\n", source.SourceAccount) + } + } + + // Show streaming sources + streamingSources := sources.GetStreamingSources() + if len(streamingSources) > 0 { + fmt.Printf(" Streaming Services:\n") + for _, source := range streamingSources { + fmt.Printf(" • %s", source.GetDisplayName()) + if source.SourceAccount != "" { + fmt.Printf(" (%s)", source.SourceAccount) + } + fmt.Println() + } + } + + return nil +} + +// selectSource handles selecting an audio source +func selectSource(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + sourceName := strings.ToUpper(c.String("source")) + sourceAccount := c.String("account") + + PrintDeviceHeader(fmt.Sprintf("Selecting source '%s'", sourceName), clientConfig.Host, clientConfig.Port) + + err = client.SelectSource(sourceName, sourceAccount) + if err != nil { + return fmt.Errorf("failed to select source: %w", err) + } + + if sourceAccount != "" { + PrintSuccess(fmt.Sprintf("Source '%s' with account '%s' selected", sourceName, sourceAccount)) + } else { + PrintSuccess(fmt.Sprintf("Source '%s' selected", sourceName)) + } + + return nil +} + +// selectSpotify handles selecting Spotify source +func selectSpotify(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Selecting Spotify source", clientConfig.Host, clientConfig.Port) + + err = client.SelectSpotify("") + if err != nil { + return fmt.Errorf("failed to select Spotify: %w", err) + } + + PrintSuccess("Spotify source selected") + return nil +} + +// selectBluetooth handles selecting Bluetooth source +func selectBluetooth(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Selecting Bluetooth source", clientConfig.Host, clientConfig.Port) + + err = client.SelectBluetooth() + if err != nil { + return fmt.Errorf("failed to select Bluetooth: %w", err) + } + + PrintSuccess("Bluetooth source selected") + return nil +} + +// selectAux handles selecting AUX input source +func selectAux(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Selecting AUX input source", clientConfig.Host, clientConfig.Port) + + err = client.SelectAux() + if err != nil { + return fmt.Errorf("failed to select AUX: %w", err) + } + + PrintSuccess("AUX input source selected") + return nil +} diff --git a/cmd/soundtouch-cli/cmd_volume.go b/cmd/soundtouch-cli/cmd_volume.go new file mode 100644 index 0000000..3c8bf63 --- /dev/null +++ b/cmd/soundtouch-cli/cmd_volume.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "time" + + "github.com/user_account/bose-soundtouch/pkg/models" + "github.com/urfave/cli/v2" +) + +// getVolume handles getting the current volume level +func getVolume(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + PrintDeviceHeader("Getting current volume", clientConfig.Host, clientConfig.Port) + + volume, err := client.GetVolume() + if err != nil { + return fmt.Errorf("failed to get volume: %w", err) + } + + fmt.Printf("Current Volume:\n") + fmt.Printf(" Device ID: %s\n", volume.DeviceID) + fmt.Printf(" Current Level: %d (%s)\n", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel())) + fmt.Printf(" Target Level: %d\n", volume.GetTargetLevel()) + fmt.Printf(" Muted: %v\n", volume.IsMuted()) + + if !volume.IsVolumeSync() { + fmt.Printf(" Note: Volume is adjusting (target: %d, actual: %d)\n", volume.GetTargetLevel(), volume.GetLevel()) + } + + return nil +} + +// setVolume handles setting the volume level +func setVolume(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + level := c.Int("level") + if level < 0 || level > 100 { + return fmt.Errorf("volume level must be between 0 and 100, got %d", level) + } + + // Safety warning for loud volumes + if level > 30 { + PrintWarning(fmt.Sprintf("Setting volume to %d (this is quite loud!)", level)) + fmt.Printf("Proceeding in 2 seconds... Press Ctrl+C to cancel\n") + time.Sleep(2 * time.Second) + } + + PrintDeviceHeader(fmt.Sprintf("Setting volume to %d", level), clientConfig.Host, clientConfig.Port) + + err = client.SetVolume(level) + if err != nil { + return fmt.Errorf("failed to set volume: %w", err) + } + + // Get updated volume to confirm + volume, err := client.GetVolume() + if err != nil { + PrintSuccess("Volume set successfully") + } else { + PrintSuccess(fmt.Sprintf("Volume set to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel()))) + } + + return nil +} + +// volumeUp handles increasing the volume +func volumeUp(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + amount := c.Int("amount") + if amount < 1 || amount > 10 { + return fmt.Errorf("volume increase amount must be between 1 and 10, got %d", amount) + } + + PrintDeviceHeader(fmt.Sprintf("Increasing volume by %d", amount), clientConfig.Host, clientConfig.Port) + + volume, err := client.IncreaseVolume(amount) + if err != nil { + return fmt.Errorf("failed to increase volume: %w", err) + } + + PrintSuccess(fmt.Sprintf("Volume increased to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel()))) + return nil +} + +// volumeDown handles decreasing the volume +func volumeDown(c *cli.Context) error { + clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + return err + } + + amount := c.Int("amount") + if amount < 1 || amount > 10 { + return fmt.Errorf("volume decrease amount must be between 1 and 10, got %d", amount) + } + + PrintDeviceHeader(fmt.Sprintf("Decreasing volume by %d", amount), clientConfig.Host, clientConfig.Port) + + volume, err := client.DecreaseVolume(amount) + if err != nil { + return fmt.Errorf("failed to decrease volume: %w", err) + } + + PrintSuccess(fmt.Sprintf("Volume decreased to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel()))) + return nil +} diff --git a/cmd/soundtouch-cli/common.go b/cmd/soundtouch-cli/common.go new file mode 100644 index 0000000..ef9452d --- /dev/null +++ b/cmd/soundtouch-cli/common.go @@ -0,0 +1,147 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/user_account/bose-soundtouch/pkg/client" + "github.com/user_account/bose-soundtouch/pkg/config" + "github.com/urfave/cli/v2" +) + +// CommonFlags defines flags that are shared across multiple commands +var CommonFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "host", + Aliases: []string{"h"}, + Usage: "SoundTouch device host/IP address (can include port like host:8090)", + EnvVars: []string{"SOUNDTOUCH_HOST"}, + }, + &cli.IntFlag{ + Name: "port", + Aliases: []string{"p"}, + Usage: "SoundTouch device port", + Value: 8090, + EnvVars: []string{"SOUNDTOUCH_PORT"}, + }, + &cli.DurationFlag{ + Name: "timeout", + Aliases: []string{"t"}, + Usage: "Request timeout", + Value: 10 * time.Second, + }, +} + +// ClientConfig holds configuration for creating a SoundTouch client +type ClientConfig struct { + Host string + Port int + Timeout time.Duration +} + +// GetClientConfig extracts client configuration from CLI context +func GetClientConfig(c *cli.Context) *ClientConfig { + host := c.String("host") + port := c.Int("port") + timeout := c.Duration("timeout") + + // Parse host:port if host contains a port + if host != "" { + if finalHost, finalPort := parseHostPort(host, port); finalHost != "" { + host = finalHost + port = finalPort + } + } + + return &ClientConfig{ + Host: host, + Port: port, + Timeout: timeout, + } +} + +// RequireHost validates that a host is provided for commands that need it +func RequireHost(c *cli.Context) error { + if c.String("host") == "" { + return fmt.Errorf("host is required. Use --host flag or set SOUNDTOUCH_HOST environment variable") + } + return nil +} + +// CreateSoundTouchClient creates a configured SoundTouch client +func CreateSoundTouchClient(config *ClientConfig) (*client.Client, error) { + cfg, err := loadConfig(config.Timeout) + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + + clientConfig := &client.Config{ + Host: config.Host, + Port: config.Port, + Timeout: cfg.HTTPTimeout, + UserAgent: cfg.UserAgent, + } + + return client.NewClient(clientConfig), nil +} + +// loadConfig loads the application configuration with optional timeout override +func loadConfig(timeout time.Duration) (*config.Config, error) { + cfg, err := config.LoadFromEnv() + if err != nil { + return nil, err + } + + // Override timeout if provided + if timeout > 0 { + cfg.HTTPTimeout = timeout + } + + return cfg, nil +} + +// parseHostPort parses a host:port string and returns host and port separately +// If no port is specified, returns the defaultPort +func parseHostPort(hostPort string, defaultPort int) (string, int) { + if !strings.Contains(hostPort, ":") { + return hostPort, defaultPort + } + + // Simple parsing - in real use, we'd use net.SplitHostPort + parts := strings.Split(hostPort, ":") + if len(parts) != 2 { + return hostPort, defaultPort + } + + host := parts[0] + portStr := parts[1] + + port, err := strconv.Atoi(portStr) + if err != nil { + return hostPort, defaultPort + } + + return host, port +} + +// PrintDeviceHeader prints a standard header for device commands +func PrintDeviceHeader(operation, host string, port int) { + fmt.Printf("%s from %s:%d...\n", operation, host, port) +} + +// PrintSuccess prints a standard success message +func PrintSuccess(message string) { + fmt.Printf("✓ %s\n", message) +} + +// PrintError prints a standard error message +func PrintError(message string) { + fmt.Printf("✗ %s\n", message) +} + +// PrintWarning prints a standard warning message +func PrintWarning(message string) { + fmt.Printf("⚠️ %s\n", message) +} diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index fbf23fd..5db746f 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -1,2376 +1,270 @@ -// Package main provides a command-line interface for controlling Bose SoundTouch devices. package main import ( - "context" - "flag" - "fmt" "log" - "net" - "strconv" - "strings" - "time" + "os" - "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" + "github.com/urfave/cli/v2" ) -// 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) { - // Check if host contains a port (has a colon) - if strings.Contains(hostPort, ":") { - host, portStr, err := net.SplitHostPort(hostPort) - if err != nil { - // If parsing fails, return original host and default port - return hostPort, defaultPort - } - - port, err := strconv.Atoi(portStr) - if err != nil || port < 1 || port > 65535 { - // If port parsing fails or is invalid, return host and default port - return host, defaultPort - } - - return host, port - } - - // No port specified, return original host and default port - return hostPort, defaultPort -} - func main() { - var ( - host = flag.String("host", "", "SoundTouch device host/IP address (can include port like host:8090)") - port = flag.Int("port", 8090, "SoundTouch device port") - timeout = flag.Duration("timeout", 10*time.Second, "Request timeout") - 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") - name = flag.Bool("name", false, "Get device name") - capabilities = flag.Bool("capabilities", false, "Get device capabilities") - 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") - stop = flag.Bool("stop", false, "Send STOP key command") - next = flag.Bool("next", false, "Send NEXT_TRACK key command") - prev = flag.Bool("prev", false, "Send PREV_TRACK key command") - volumeUp = flag.Bool("volume-up", false, "Send VOLUME_UP key command") - volumeDown = flag.Bool("volume-down", false, "Send VOLUME_DOWN key command") - power = flag.Bool("power", false, "Send POWER key command") - mute = flag.Bool("mute", false, "Send MUTE key command") - thumbsUp = flag.Bool("thumbs-up", false, "Send THUMBS_UP key command") - thumbsDown = flag.Bool("thumbs-down", false, "Send THUMBS_DOWN key command") - preset = flag.Int("preset", 0, "Select preset (1-6)") - volume = flag.Bool("volume", false, "Get current volume level") - setVolume = flag.Int("set-volume", -1, "Set volume level (0-100)") - incVolume = flag.Int("inc-volume", 0, "Increase volume by amount (1-10, default: 2)") - decVolume = flag.Int("dec-volume", 0, "Decrease volume by amount (1-10, default: 2)") - bass = flag.Bool("bass", false, "Get current bass level") - setBass = flag.Int("set-bass", -99, "Set bass level (-9 to +9)") - incBass = flag.Int("inc-bass", 0, "Increase bass by amount (1-3, default: 1)") - decBass = flag.Int("dec-bass", 0, "Decrease bass by amount (1-3, default: 1)") - balance = flag.Bool("balance", false, "Get current balance level") - setBalance = flag.Int("set-balance", -99, "Set balance level (-50 to +50)") - incBalance = flag.Int("inc-balance", 0, "Increase balance by amount (1-10, default: 5)") - decBalance = flag.Int("dec-balance", 0, "Decrease balance by amount (1-10, default: 5)") - selectSource = flag.String("select-source", "", "Select audio source (SPOTIFY, BLUETOOTH, AUX, TUNEIN, PANDORA, AMAZON, IHEARTRADIO, STORED_MUSIC)") - sourceAccount = flag.String("source-account", "", "Source account for streaming services (optional)") - spotify = flag.Bool("spotify", false, "Select Spotify source") - bluetooth = flag.Bool("bluetooth", false, "Select Bluetooth source") - aux = flag.Bool("aux", false, "Select AUX input source") - clockTime = flag.Bool("clock-time", false, "Get device clock time") - setClockTime = flag.String("set-clock-time", "", "Set device clock time (format: 'now' or Unix timestamp)") - clockDisplay = flag.Bool("clock-display", false, "Get clock display settings") - enableClock = flag.Bool("enable-clock", false, "Enable clock display") - disableClock = flag.Bool("disable-clock", false, "Disable clock display") - clockFormat = flag.String("clock-format", "", "Set clock display format (12, 24, auto)") - clockBright = flag.Int("clock-brightness", -1, "Set clock display brightness (0-100)") - networkInfo = flag.Bool("network-info", false, "Get network information") - zone = flag.Bool("zone", false, "Get current zone configuration") - zoneStatus = flag.Bool("zone-status", false, "Get zone status for this device") - zoneMembers = flag.Bool("zone-members", false, "List all devices in current zone") - createZone = flag.String("create-zone", "", "Create zone with device IDs (comma-separated)") - addToZone = flag.String("add-to-zone", "", "Add device to zone (format: deviceID@ip or deviceID)") - removeFromZone = flag.String("remove-from-zone", "", "Remove device from zone (device ID)") - dissolveZone = flag.Bool("dissolve-zone", false, "Dissolve current zone (make standalone)") - setName = flag.String("set-name", "", "Set device name") - bassCapabilities = flag.Bool("bass-capabilities", false, "Get bass capabilities") - trackInfo = flag.Bool("track-info", false, "Get track information") - help = flag.Bool("help", false, "Show help") - ) - - flag.Parse() - - if *help { - printHelp() - return + app := &cli.App{ + Name: "soundtouch-cli", + Usage: "Command-line interface for controlling Bose SoundTouch devices", + Description: `A comprehensive CLI tool for interacting with Bose SoundTouch devices. + Supports device discovery, playback control, volume/bass/balance adjustment, + source selection, zone management, and more.`, + Version: "1.0.0", + Authors: []*cli.Author{ + { + Name: "SoundTouch CLI Contributors", + Email: "info@example.com", + }, + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"v"}, + Usage: "Enable verbose output", + }, + }, + Commands: []*cli.Command{ + // Discovery commands + { + Name: "discover", + Aliases: []string{"d"}, + Usage: "Discover SoundTouch devices on the network", + Subcommands: []*cli.Command{ + { + Name: "devices", + Usage: "Discover and list SoundTouch devices", + Action: discoverDevices, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Aliases: []string{"a"}, + Usage: "Show detailed information for all devices", + }, + &cli.DurationFlag{ + Name: "timeout", + Aliases: []string{"t"}, + Usage: "Discovery timeout", + Value: 10000000000, // 10 seconds in nanoseconds + }, + }, + }, + }, + }, + // Device information commands + { + Name: "info", + Aliases: []string{"i"}, + Usage: "Get device information", + Action: getDeviceInfo, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "name", + Usage: "Get or set device name", + Flags: CommonFlags, + Before: RequireHost, + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get device name", + Action: getDeviceName, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "set", + Usage: "Set device name", + Action: setDeviceName, + Flags: append(CommonFlags, &cli.StringFlag{ + Name: "value", + Aliases: []string{"n"}, + Usage: "New device name", + Required: true, + }), + Before: RequireHost, + }, + }, + }, + { + Name: "capabilities", + Usage: "Get device capabilities", + Action: getCapabilities, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "presets", + Usage: "Get configured presets", + Action: getPresets, + Flags: CommonFlags, + Before: RequireHost, + }, + // Playback commands + { + Name: "play", + Aliases: []string{"p"}, + Usage: "Playback control commands", + Subcommands: []*cli.Command{ + { + Name: "now", + Usage: "Get current playback status", + Action: getNowPlaying, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "start", + Usage: "Start playback", + Action: playCommand, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "pause", + Usage: "Pause playback", + Action: pauseCommand, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "stop", + Usage: "Stop playback", + Action: stopCommand, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "next", + Usage: "Next track", + Action: nextCommand, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "prev", + Usage: "Previous track", + Action: prevCommand, + Flags: CommonFlags, + Before: RequireHost, + }, + }, + }, + // Volume commands + { + Name: "volume", + Aliases: []string{"vol"}, + Usage: "Volume control commands", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get current volume level", + Action: getVolume, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "set", + Usage: "Set volume level", + Action: setVolume, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "level", + Aliases: []string{"l"}, + Usage: "Volume level (0-100)", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "up", + Usage: "Increase volume", + Action: volumeUp, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "amount", + Aliases: []string{"a"}, + Usage: "Amount to increase (1-10)", + Value: 2, + }), + Before: RequireHost, + }, + { + Name: "down", + Usage: "Decrease volume", + Action: volumeDown, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "amount", + Aliases: []string{"a"}, + Usage: "Amount to decrease (1-10)", + Value: 2, + }), + Before: RequireHost, + }, + }, + }, + // Source commands + { + Name: "source", + Aliases: []string{"src"}, + Usage: "Audio source commands", + Subcommands: []*cli.Command{ + { + Name: "list", + Usage: "List available audio sources", + Action: listSources, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "select", + Usage: "Select an audio source", + Action: selectSource, + Flags: append(CommonFlags, + &cli.StringFlag{ + Name: "source", + Aliases: []string{"s"}, + Usage: "Source to select (SPOTIFY, BLUETOOTH, AUX, etc.)", + Required: true, + }, + &cli.StringFlag{ + Name: "account", + Aliases: []string{"a"}, + Usage: "Source account for streaming services (optional)", + }, + ), + Before: RequireHost, + }, + { + Name: "spotify", + Usage: "Select Spotify source", + Action: selectSpotify, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "bluetooth", + Usage: "Select Bluetooth source", + Action: selectBluetooth, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "aux", + Usage: "Select AUX input source", + Action: selectAux, + Flags: CommonFlags, + Before: RequireHost, + }, + }, + }, + }, } - // If no specific action is requested, show help - if !*discover && !*discoverAll && !*info && !*nowPlaying && !*sources && !*name && !*capabilities && !*presets && *key == "" && !*play && !*pause && !*stop && !*next && !*prev && !*volumeUp && !*volumeDown && !*power && !*mute && !*thumbsUp && !*thumbsDown && *preset == 0 && !*volume && *setVolume == -1 && *incVolume == 0 && *decVolume == 0 && !*bass && *setBass == -99 && *incBass == 0 && *decBass == 0 && !*balance && *setBalance == -99 && *incBalance == 0 && *decBalance == 0 && *selectSource == "" && !*spotify && !*bluetooth && !*aux && !*clockTime && *setClockTime == "" && !*clockDisplay && !*enableClock && !*disableClock && *clockFormat == "" && *clockBright == -1 && !*networkInfo && !*zone && !*zoneStatus && !*zoneMembers && *createZone == "" && *addToZone == "" && *removeFromZone == "" && !*dissolveZone && *setName == "" && !*bassCapabilities && !*trackInfo && *host == "" { - printHelp() - return - } - - // Parse host:port if provided - var ( - finalHost string - finalPort int - ) - - if *host != "" { - finalHost, finalPort = parseHostPort(*host, *port) - } - - // Handle discovery - if *discover || *discoverAll { - if err := handleDiscovery(*discoverAll, *timeout); err != nil { - log.Fatalf("Discovery failed: %v", err) - } - - return - } - - // Handle device info - if *info { - if *host == "" { - log.Fatal("Host is required for info command. Use -host flag or -discover to find devices.") - } - - if err := handleDeviceInfo(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get device info: %v", err) - } - - 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(finalHost, finalPort, *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(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get sources: %v", err) - } - - return - } - - // Handle name - if *name { - if *host == "" { - log.Fatal("Host is required for name command. Use -host flag or -discover to find devices.") - } - - if err := handleName(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get device name: %v", err) - } - - return - } - - // Handle capabilities - if *capabilities { - if *host == "" { - log.Fatal("Host is required for capabilities command. Use -host flag or -discover to find devices.") - } - - if err := handleCapabilities(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get device capabilities: %v", err) - } - - return - } - - // Handle presets - if *presets { - if *host == "" { - log.Fatal("Host is required for presets command. Use -host flag or -discover to find devices.") - } - - if err := handlePresets(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get presets: %v", err) - } - - return - } - - // Handle key commands - if *key != "" || *play || *pause || *stop || *next || *prev || *volumeUp || *volumeDown || *power || *mute || *thumbsUp || *thumbsDown || *preset > 0 { - if *host == "" { - log.Fatal("Host is required for key commands. Use -host flag or -discover to find devices.") - } - - if err := handleKeyCommands(finalHost, finalPort, *timeout, *key, *play, *pause, *stop, *next, *prev, *volumeUp, *volumeDown, *power, *mute, *thumbsUp, *thumbsDown, *preset); err != nil { - log.Fatalf("Failed to send key command: %v", err) - } - - return - } - - // Handle volume commands - if *volume || *setVolume != -1 || *incVolume > 0 || *decVolume > 0 { - if *host == "" { - log.Fatal("Host is required for volume commands. Use -host flag or -discover to find devices.") - } - - if err := handleVolumeCommands(finalHost, finalPort, *timeout, *volume, *setVolume, *incVolume, *decVolume); err != nil { - log.Fatalf("Failed to execute volume command: %v", err) - } - - return - } - - // Handle bass commands - if *bass || *setBass != -99 || *incBass > 0 || *decBass > 0 { - if *host == "" { - log.Fatal("Host is required for bass commands. Use -host flag or -discover to find devices.") - } - - if err := handleBassCommands(finalHost, finalPort, *timeout, *bass, *setBass, *incBass, *decBass); err != nil { - log.Fatalf("Failed to execute bass command: %v", err) - } - - return - } - - // Handle balance commands - if *balance || *setBalance != -99 || *incBalance > 0 || *decBalance > 0 { - if *host == "" { - log.Fatal("Host is required for balance commands. Use -host flag or -discover to find devices.") - } - - if err := handleBalanceCommands(finalHost, finalPort, *timeout, *balance, *setBalance, *incBalance, *decBalance); err != nil { - log.Fatalf("Failed to execute balance command: %v", err) - } - - return - } - - // Handle source selection commands - if *selectSource != "" || *spotify || *bluetooth || *aux { - if *host == "" { - log.Fatal("Host is required for source selection. Use -host flag or -discover to find devices.") - } - - if err := handleSourceCommands(finalHost, finalPort, *timeout, *selectSource, *sourceAccount, *spotify, *bluetooth, *aux); err != nil { - log.Fatalf("Failed to select source: %v", err) - } - - return - } - - // Handle clock/time commands - if *clockTime || *setClockTime != "" || *clockDisplay || *enableClock || *disableClock || *clockFormat != "" || *clockBright != -1 { - if *host == "" { - log.Fatal("Host is required for clock/time commands. Use -host flag or -discover to find devices.") - } - - if err := handleClockCommands(finalHost, finalPort, *timeout, *clockTime, *setClockTime, *clockDisplay, *enableClock, *disableClock, *clockFormat, *clockBright); err != nil { - log.Fatalf("Failed to execute clock command: %v", err) - } - - return - } - - // Handle network info command - if *networkInfo { - if *host == "" { - log.Fatal("Host is required for network info command. Use -host flag or -discover to find devices.") - } - - if err := handleNetworkInfo(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get network info: %v", err) - } - - return - } - - // Handle zone commands - if *zone || *zoneStatus || *zoneMembers || *createZone != "" || *addToZone != "" || *removeFromZone != "" || *dissolveZone { - if *host == "" { - log.Fatal("Host is required for zone commands. Use -host flag or -discover to find devices.") - } - - if err := handleZoneCommands(finalHost, finalPort, *timeout, *zone, *zoneStatus, *zoneMembers, *createZone, *addToZone, *removeFromZone, *dissolveZone); err != nil { - log.Fatalf("Failed to execute zone command: %v", err) - } - - return - } - - // Handle set name command - if *setName != "" { - if *host == "" { - log.Fatal("Host is required for set-name command. Use -host flag or -discover to find devices.") - } - - if err := handleSetName(finalHost, finalPort, *timeout, *setName); err != nil { - log.Fatalf("Failed to set device name: %v", err) - } - - return - } - - // Handle bass capabilities command - if *bassCapabilities { - if *host == "" { - log.Fatal("Host is required for bass-capabilities command. Use -host flag or -discover to find devices.") - } - - if err := handleBassCapabilities(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get bass capabilities: %v", err) - } - - return - } - - // Handle track info command - if *trackInfo { - if *host == "" { - log.Fatal("Host is required for track-info command. Use -host flag or -discover to find devices.") - } - - if err := handleTrackInfo(finalHost, finalPort, *timeout); err != nil { - log.Fatalf("Failed to get track info: %v", err) - } - - return + if err := app.Run(os.Args); err != nil { + log.Fatal(err) } } - -// handleSetName sets the device name -func handleSetName(host string, port int, timeout time.Duration, name string) error { - config := &client.Config{ - Host: host, - Port: port, - Timeout: timeout, - } - - client := client.NewClient(config) - - fmt.Printf("Setting device name to '%s'...\n", name) - - if err := client.SetName(name); err != nil { - return fmt.Errorf("failed to set device name: %w", err) - } - - fmt.Println("✅ Device name set successfully") - - return nil -} - -// handleBassCapabilities gets the bass capabilities -func handleBassCapabilities(host string, port int, timeout time.Duration) error { - config := &client.Config{ - Host: host, - Port: port, - Timeout: timeout, - } - - client := client.NewClient(config) - - capabilities, err := client.GetBassCapabilities() - if err != nil { - return fmt.Errorf("failed to get bass capabilities: %w", err) - } - - fmt.Printf("Bass Capabilities:\n") - - if capabilities.IsBassSupported() { - fmt.Printf(" Bass Control: ✅ Supported\n") - fmt.Printf(" Range: %d to %d\n", capabilities.GetMinLevel(), capabilities.GetMaxLevel()) - fmt.Printf(" Default: %d\n", capabilities.GetDefaultLevel()) - } else { - fmt.Printf(" Bass Control: ❌ Not supported\n") - } - - return nil -} - -// handleTrackInfo gets the track information -func handleTrackInfo(host string, port int, timeout time.Duration) error { - config := &client.Config{ - Host: host, - Port: port, - Timeout: timeout, - } - - client := client.NewClient(config) - - trackInfo, err := client.GetTrackInfo() - if err != nil { - return fmt.Errorf("failed to get track info: %w", err) - } - - fmt.Printf("Track Information:\n") - fmt.Printf(" Source: %s\n", trackInfo.Source) - - if trackInfo.Track != "" { - fmt.Printf(" Track: %s\n", trackInfo.Track) - } - - if trackInfo.Artist != "" { - fmt.Printf(" Artist: %s\n", trackInfo.Artist) - } - - if trackInfo.Album != "" { - fmt.Printf(" Album: %s\n", trackInfo.Album) - } - - if trackInfo.StationName != "" { - fmt.Printf(" Station: %s\n", trackInfo.StationName) - } - - fmt.Printf(" Play Status: %s\n", trackInfo.PlayStatus) - - return nil -} - -func printHelp() { - fmt.Println("SoundTouch CLI - Test tool for Bose SoundTouch API") - fmt.Println() - fmt.Println("Usage:") - fmt.Println(" soundtouch-cli [options]") - fmt.Println() - fmt.Println("Options:") - fmt.Println(" -host SoundTouch device IP address (or host:port)") - fmt.Println(" -port SoundTouch device port (default: 8090)") - fmt.Println(" -timeout Request timeout (default: 10s)") - 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(" -name Get device name (requires -host)") - fmt.Println(" -capabilities Get device capabilities (requires -host)") - fmt.Println(" -presets Get configured presets (requires -host)") - fmt.Println(" -key Send key command (requires -host)") - fmt.Println(" Available keys: PLAY, PAUSE, STOP, PREV_TRACK, NEXT_TRACK") - fmt.Println(" THUMBS_UP, THUMBS_DOWN, BOOKMARK, POWER, MUTE") - fmt.Println(" VOLUME_UP, VOLUME_DOWN, PRESET_1-6, AUX_INPUT") - fmt.Println(" SHUFFLE_OFF, SHUFFLE_ON, REPEAT_OFF, REPEAT_ONE, REPEAT_ALL") - fmt.Println(" -play Send PLAY key command (requires -host)") - fmt.Println(" -pause Send PAUSE key command (requires -host)") - fmt.Println(" -stop Send STOP key command (requires -host)") - fmt.Println(" -next Send NEXT_TRACK key command (requires -host)") - fmt.Println(" -prev Send PREV_TRACK key command (requires -host)") - fmt.Println(" -volume-up Send VOLUME_UP key command (requires -host)") - fmt.Println(" -volume-down Send VOLUME_DOWN key command (requires -host)") - fmt.Println(" -power Send POWER key command (requires -host)") - fmt.Println(" -mute Send MUTE key command (requires -host)") - fmt.Println(" -thumbs-up Send THUMBS_UP key command (requires -host)") - fmt.Println(" -thumbs-down Send THUMBS_DOWN key command (requires -host)") - fmt.Println(" -preset <1-6> Select preset (requires -host)") - fmt.Println(" -volume Get current volume level (requires -host)") - fmt.Println(" -set-volume <0-100> Set volume level (requires -host)") - fmt.Println(" -inc-volume Increase volume by amount (1-10, default: 2)") - fmt.Println(" -dec-volume Decrease volume by amount (1-10, default: 2)") - fmt.Println() - fmt.Println("Bass Control:") - fmt.Println(" -bass Get current bass level (requires -host)") - fmt.Println(" -set-bass <-9-+9> Set bass level (requires -host)") - fmt.Println(" -inc-bass Increase bass by amount (1-3, default: 1)") - fmt.Println(" -dec-bass Decrease bass by amount (1-3, default: 1)") - fmt.Println() - fmt.Println("Balance Control:") - fmt.Println(" -balance Get current balance level (requires -host)") - fmt.Println(" -set-balance <-50-+50> Set balance level (requires -host)") - fmt.Println(" -inc-balance Increase balance by amount (1-10, default: 5)") - fmt.Println(" -dec-balance Decrease balance by amount (1-10, default: 5)") - fmt.Println() - fmt.Println("Source Selection:") - fmt.Println(" -select-source Select audio source (requires -host)") - fmt.Println(" Available: SPOTIFY, BLUETOOTH, AUX, TUNEIN, PANDORA, AMAZON, IHEARTRADIO, STORED_MUSIC") - fmt.Println(" -source-account Source account for streaming services (optional)") - fmt.Println(" -spotify Select Spotify source (requires -host)") - fmt.Println(" -bluetooth Select Bluetooth source (requires -host)") - fmt.Println(" -aux Select AUX input source (requires -host)") - fmt.Println() - fmt.Println("System Information:") - fmt.Println(" -clock-time Get device clock time (requires -host)") - fmt.Println(" -set-clock-time