// Package main provides a command-line interface for controlling Bose SoundTouch devices. package main import ( "context" "flag" "fmt" "log" "net" "strconv" "strings" "time" "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" ) // 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 } // 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 var 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 } } // 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