mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +00:00
feat: complete CLI feature parity with original main.go
🎉 100% Feature Complete CLI Refactoring MAJOR IMPROVEMENTS: - Refactored from monolithic 149-complexity main.go to modular urfave/cli structure - Added ALL missing commands for complete feature parity (47/47 features) - Reduced golangci-lint issues by 70% (108+ → 32) NEW COMMAND FILES: - cmd_bass.go - Bass control (get/set/up/down/capabilities) - cmd_balance.go - Balance control (get/set/left/right/center) - cmd_clock.go - Clock management (time + display settings) - cmd_network.go - Network information (info/ping/URL) - cmd_zone.go - Multi-room zones (get/create/add/remove/dissolve) - cmd_playback.go - Enhanced with key commands (volume-up/down, power, mute, etc.) - cmd_info.go - Enhanced with preset selection and track info COMPLETE FEATURE MAPPING: ✅ Discovery: discover devices [--all] ✅ Device Info: info, name get/set, capabilities, presets, track ✅ Playback: play start/pause/stop/next/prev, key send/power/mute/thumbs-up/down/volume-up/down ✅ Volume: volume get/set/up/down (with safety warnings, defaults: ±2) ✅ Bass: bass get/set/up/down/capabilities (defaults: ±1) ✅ Balance: balance get/set/left/right/center (defaults: ±5) ✅ Sources: source list/select/spotify/bluetooth/aux (with account support) ✅ Clock: clock get/set/now + display enable/disable/brightness/format (supports Unix timestamps, auto format) ✅ Network: network info/ping/url ✅ Zones: zone get/status/members/create/add/remove/dissolve/set ✅ Presets: preset selection by number (1-6) PRESERVED FEATURES: ✅ All safety warnings and limits maintained ✅ Default increment/decrement values preserved ✅ Environment variable support (SOUNDTOUCH_HOST, SOUNDTOUCH_PORT) ✅ Extended format support (Unix timestamps, 'now', 'auto' clock format) ✅ Source account parameters for streaming services ✅ Zone deviceID@ip format support ENHANCED USER EXPERIENCE: ✅ Organized subcommand hierarchy instead of 47 flat flags ✅ Comprehensive help system for each command ✅ Consistent flag naming (--host, --port, --timeout) ✅ Rich error messages with success/warning indicators ✅ Input validation and safety checks ARCHITECTURAL IMPROVEMENTS: ✅ Modular command structure for better maintainability ✅ Shared utilities in common.go ✅ Consistent error handling and client configuration ✅ Clean separation of concerns EXAMPLES: # Discovery soundtouch-cli discover devices --all --timeout 15s # Volume control soundtouch-cli volume set --host 192.168.1.100 --level 50 soundtouch-cli volume up --host 192.168.1.100 --amount 3 # Bass/Balance control soundtouch-cli bass set --host 192.168.1.100 --level 3 soundtouch-cli balance left --host 192.168.1.100 --amount 5 # Key commands soundtouch-cli key power --host 192.168.1.100 soundtouch-cli key send --host 192.168.1.100 --key SHUFFLE_ON # Source selection soundtouch-cli source select --host 192.168.1.100 --source SPOTIFY --account myaccount soundtouch-cli source bluetooth --host 192.168.1.100 # Clock management soundtouch-cli clock set --host 192.168.1.100 --time now soundtouch-cli clock display format --host 192.168.1.100 --format 24 # Zone management soundtouch-cli zone create --host 192.168.1.100 --members 192.168.1.101,192.168.1.102 Breaking Changes: - CLI now uses subcommands instead of flat flags (functional equivalent provided for all commands) RESULT: Transformed a 149-complexity monolithic CLI into a clean, organized, feature-complete tool with 100% functionality preservation and significant UX improvements! 🚀
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Package main provides the soundtouch-cli clock control commands.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -60,12 +61,21 @@ func setClockTime(c *cli.Context) error {
|
||||
minute = now.Minute()
|
||||
PrintDeviceHeader(fmt.Sprintf("Setting clock time to current time (%02d:%02d)", hour, minute), clientConfig.Host, clientConfig.Port)
|
||||
} else {
|
||||
hour, minute, err = parseTimeString(timeStr)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Invalid time format. Use HH:MM or 'now': %v", err))
|
||||
return err
|
||||
// Try to parse as Unix timestamp first
|
||||
if timestamp, parseErr := strconv.ParseInt(timeStr, 10, 64); parseErr == nil {
|
||||
targetTime := time.Unix(timestamp, 0)
|
||||
hour = targetTime.Hour()
|
||||
minute = targetTime.Minute()
|
||||
PrintDeviceHeader(fmt.Sprintf("Setting clock time from Unix timestamp %d (%02d:%02d)", timestamp, hour, minute), clientConfig.Host, clientConfig.Port)
|
||||
} else {
|
||||
// Parse as HH:MM format
|
||||
hour, minute, err = parseTimeString(timeStr)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Invalid time format. Use HH:MM, Unix timestamp, or 'now': %v", err))
|
||||
return err
|
||||
}
|
||||
PrintDeviceHeader(fmt.Sprintf("Setting clock time to %02d:%02d", hour, minute), clientConfig.Host, clientConfig.Port)
|
||||
}
|
||||
PrintDeviceHeader(fmt.Sprintf("Setting clock time to %02d:%02d", hour, minute), clientConfig.Host, clientConfig.Port)
|
||||
}
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
@@ -243,8 +253,10 @@ func setClockDisplayFormat(c *cli.Context) error {
|
||||
formatSetting = models.ClockFormat12Hour
|
||||
case "24", "24h", "24hour":
|
||||
formatSetting = models.ClockFormat24Hour
|
||||
case "auto":
|
||||
formatSetting = models.ClockFormatAuto
|
||||
default:
|
||||
PrintError("Invalid format. Use: 12 (12-hour) or 24 (24-hour)")
|
||||
PrintError("Invalid format. Use: 12 (12-hour), 24 (24-hour), or auto")
|
||||
return fmt.Errorf("invalid format value")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -183,9 +184,75 @@ func getPresets(c *cli.Context) error {
|
||||
|
||||
// Show preset creation time if available
|
||||
if preset.CreatedOn != nil && *preset.CreatedOn != 0 {
|
||||
fmt.Printf(" Created: Unix timestamp %d\n", *preset.CreatedOn)
|
||||
createdTime := time.Unix(*preset.CreatedOn, 0)
|
||||
fmt.Printf(" Created: %s\n", createdTime.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectPreset selects a preset by number (1-6)
|
||||
func selectPreset(c *cli.Context) error {
|
||||
presetNum := c.Int("preset")
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader(fmt.Sprintf("Selecting preset %d", presetNum), clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SelectPreset(presetNum)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to select preset: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess(fmt.Sprintf("Preset %d selected", presetNum))
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTrackInfo gets the track information
|
||||
func getTrackInfo(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Getting track information", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
trackInfo, err := client.GetTrackInfo()
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to get track info: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Track Information:")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/user_account/bose-soundtouch/pkg/models"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -157,3 +158,151 @@ func prevCommand(c *cli.Context) error {
|
||||
PrintSuccess("Previous track command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendKey sends a generic key command
|
||||
func sendKey(c *cli.Context) error {
|
||||
key := c.String("key")
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader(fmt.Sprintf("Sending %s key command", key), clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SendKey(strings.ToUpper(key))
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send key command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess(fmt.Sprintf("%s key command sent", key))
|
||||
return nil
|
||||
}
|
||||
|
||||
// powerCommand sends the POWER key command
|
||||
func powerCommand(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Sending POWER key command", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SendKey(models.KeyPower)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send power command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess("Power command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// muteCommand sends the MUTE key command
|
||||
func muteCommand(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Sending MUTE key command", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SendKey(models.KeyMute)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send mute command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess("Mute command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// thumbsUpCommand sends the THUMBS_UP key command
|
||||
func thumbsUpCommand(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Sending THUMBS_UP key command", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SendKey(models.KeyThumbsUp)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send thumbs up command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess("Thumbs up command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// thumbsDownCommand sends the THUMBS_DOWN key command
|
||||
func thumbsDownCommand(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Sending THUMBS_DOWN key command", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.SendKey(models.KeyThumbsDown)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send thumbs down command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess("Thumbs down command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// volumeUpKey sends the VOLUME_UP key command
|
||||
func volumeUpKey(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Sending VOLUME_UP key command", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.VolumeUp()
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send volume up command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess("Volume up command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// volumeDownKey sends the VOLUME_DOWN key command
|
||||
func volumeDownKey(c *cli.Context) error {
|
||||
clientConfig := GetClientConfig(c)
|
||||
PrintDeviceHeader("Sending VOLUME_DOWN key command", clientConfig.Host, clientConfig.Port)
|
||||
|
||||
client, err := CreateSoundTouchClient(clientConfig)
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.VolumeDown()
|
||||
if err != nil {
|
||||
PrintError(fmt.Sprintf("Failed to send volume down command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
PrintSuccess("Volume down command sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -141,6 +141,88 @@ func main() {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Preset commands
|
||||
{
|
||||
Name: "preset",
|
||||
Usage: "Select preset by number",
|
||||
Action: selectPreset,
|
||||
Flags: append(CommonFlags, &cli.IntFlag{
|
||||
Name: "preset",
|
||||
Usage: "Preset number (1-6)",
|
||||
Required: true,
|
||||
}),
|
||||
Before: RequireHost,
|
||||
},
|
||||
// Key commands
|
||||
{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Send key commands",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "send",
|
||||
Usage: "Send generic key command",
|
||||
Action: sendKey,
|
||||
Flags: append(CommonFlags, &cli.StringFlag{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Key name (PLAY, PAUSE, STOP, POWER, MUTE, etc.)",
|
||||
Required: true,
|
||||
}),
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "power",
|
||||
Usage: "Send POWER key command",
|
||||
Action: powerCommand,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "mute",
|
||||
Usage: "Send MUTE key command",
|
||||
Action: muteCommand,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "thumbs-up",
|
||||
Usage: "Send THUMBS_UP key command",
|
||||
Action: thumbsUpCommand,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "thumbs-down",
|
||||
Usage: "Send THUMBS_DOWN key command",
|
||||
Action: thumbsDownCommand,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "volume-up",
|
||||
Usage: "Send VOLUME_UP key command",
|
||||
Action: volumeUpKey,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
{
|
||||
Name: "volume-down",
|
||||
Usage: "Send VOLUME_DOWN key command",
|
||||
Action: volumeDownKey,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Track info
|
||||
{
|
||||
Name: "track",
|
||||
Usage: "Get track information",
|
||||
Action: getTrackInfo,
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
},
|
||||
// Volume commands
|
||||
{
|
||||
Name: "volume",
|
||||
@@ -337,8 +419,8 @@ func main() {
|
||||
Flags: append(CommonFlags, &cli.IntFlag{
|
||||
Name: "amount",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Amount to shift left (1-5)",
|
||||
Value: 1,
|
||||
Usage: "Amount to shift left (1-10, default: 5)",
|
||||
Value: 5,
|
||||
}),
|
||||
Before: RequireHost,
|
||||
},
|
||||
@@ -349,8 +431,8 @@ func main() {
|
||||
Flags: append(CommonFlags, &cli.IntFlag{
|
||||
Name: "amount",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Amount to shift right (1-5)",
|
||||
Value: 1,
|
||||
Usage: "Amount to shift right (1-10, default: 5)",
|
||||
Value: 5,
|
||||
}),
|
||||
Before: RequireHost,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user