mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
feat: refactor soundtouch-cli to use urfave/cli framework
- Added urfave/cli/v2 dependency for better CLI structure - Created modular command structure with separate files: - common.go: Shared utilities and client setup - cmd_discover.go: Device discovery commands - cmd_info.go: Device information commands - cmd_volume.go: Volume control commands - cmd_playback.go: Playback control commands - cmd_source.go: Source selection commands - Replaced giant main() function (complexity 149) with organized subcommands - Added proper flag handling and validation - Improved help text and user experience WIP: Some issues remain (flag conflicts, missing commands) Next: Complete remaining commands and fix conflicts
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+258
-2364
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,11 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
@@ -7,6 +9,12 @@ github.com/hashicorp/mdns v1.0.6/go.mod h1:X4+yWh+upFECLOki1doUPaKpgNQII9gy4bUdC
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user