diff --git a/cmd/example-upnp/main.go b/cmd/example-upnp/main.go index 7a880df..f2bf878 100644 --- a/cmd/example-upnp/main.go +++ b/cmd/example-upnp/main.go @@ -58,7 +58,7 @@ func main() { } // Use the configured discovery service to isolate UPnP - configuredService := discovery.NewDiscoveryServiceWithConfig(cfg) + configuredService := discovery.NewServiceWithConfig(cfg) devices, err := configuredService.DiscoverDevices(ctx) duration := time.Since(start) diff --git a/cmd/soundtouch-cli/cmd_balance.go b/cmd/soundtouch-cli/cmd_balance.go index 944b086..74d7696 100644 --- a/cmd/soundtouch-cli/cmd_balance.go +++ b/cmd/soundtouch-cli/cmd_balance.go @@ -62,6 +62,7 @@ func setBalance(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Balance level set to %d", level)) + return nil } @@ -85,6 +86,7 @@ func balanceLeft(c *cli.Context) error { } newLevel := currentBalance.ActualBalance - amount + err = client.SetBalanceSafe(newLevel) if err != nil { PrintError(fmt.Sprintf("Failed to shift balance left: %v", err)) @@ -92,6 +94,7 @@ func balanceLeft(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Balance shifted from %d to %d (left)", currentBalance.ActualBalance, newLevel)) + return nil } @@ -115,6 +118,7 @@ func balanceRight(c *cli.Context) error { } newLevel := currentBalance.ActualBalance + amount + err = client.SetBalanceSafe(newLevel) if err != nil { PrintError(fmt.Sprintf("Failed to shift balance right: %v", err)) @@ -122,6 +126,7 @@ func balanceRight(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Balance shifted from %d to %d (right)", currentBalance.ActualBalance, newLevel)) + return nil } @@ -143,5 +148,6 @@ func balanceCenter(c *cli.Context) error { } PrintSuccess("Balance centered") + return nil } diff --git a/cmd/soundtouch-cli/cmd_bass.go b/cmd/soundtouch-cli/cmd_bass.go index 52456fa..010fb36 100644 --- a/cmd/soundtouch-cli/cmd_bass.go +++ b/cmd/soundtouch-cli/cmd_bass.go @@ -51,6 +51,7 @@ func setBass(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Bass level set to %d", level)) + return nil } @@ -74,6 +75,7 @@ func bassUp(c *cli.Context) error { } newLevel := currentBass.ActualBass + amount + err = client.SetBassSafe(newLevel) if err != nil { PrintError(fmt.Sprintf("Failed to increase bass: %v", err)) @@ -81,6 +83,7 @@ func bassUp(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Bass increased from %d to %d", currentBass.ActualBass, newLevel)) + return nil } @@ -104,6 +107,7 @@ func bassDown(c *cli.Context) error { } newLevel := currentBass.ActualBass - amount + err = client.SetBassSafe(newLevel) if err != nil { PrintError(fmt.Sprintf("Failed to decrease bass: %v", err)) @@ -111,6 +115,7 @@ func bassDown(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Bass decreased from %d to %d", currentBass.ActualBass, newLevel)) + return nil } diff --git a/cmd/soundtouch-cli/cmd_clock.go b/cmd/soundtouch-cli/cmd_clock.go index 0a62eb5..ecf6cf2 100644 --- a/cmd/soundtouch-cli/cmd_clock.go +++ b/cmd/soundtouch-cli/cmd_clock.go @@ -53,6 +53,7 @@ func setClockTime(c *cli.Context) error { // Parse time string (HH:MM format) var hour, minute int + var err error if timeStr == "now" { @@ -74,6 +75,7 @@ func setClockTime(c *cli.Context) error { 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) } } @@ -89,6 +91,7 @@ func setClockTime(c *cli.Context) error { targetTime := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()) clockTimeRequest := models.NewClockTimeRequest(targetTime) + err = client.SetClockTime(clockTimeRequest) if err != nil { PrintError(fmt.Sprintf("Failed to set clock time: %v", err)) @@ -96,6 +99,7 @@ func setClockTime(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Clock time set to %02d:%02d", hour, minute)) + return nil } @@ -118,6 +122,7 @@ func setClockTimeNow(c *cli.Context) error { now := time.Now() PrintSuccess(fmt.Sprintf("Clock time set to current time (%02d:%02d)", now.Hour(), now.Minute())) + return nil } @@ -172,6 +177,7 @@ func enableClockDisplay(c *cli.Context) error { } PrintSuccess("Clock display enabled") + return nil } @@ -193,6 +199,7 @@ func disableClockDisplay(c *cli.Context) error { } PrintSuccess("Clock display disabled") + return nil } @@ -210,6 +217,7 @@ func setClockDisplayBrightness(c *cli.Context) error { // Convert brightness string to numeric value var brightnessLevel int + switch brightness { case "low", "LOW": brightnessLevel = 25 @@ -231,6 +239,7 @@ func setClockDisplayBrightness(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Clock display brightness set to %s", brightness)) + return nil } @@ -248,6 +257,7 @@ func setClockDisplayFormat(c *cli.Context) error { // Validate and normalize format value var formatSetting models.ClockFormat + switch format { case "12", "12h", "12hour": formatSetting = models.ClockFormat12Hour @@ -267,6 +277,7 @@ func setClockDisplayFormat(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Clock display format set to %s", format)) + return nil } diff --git a/cmd/soundtouch-cli/cmd_discover.go b/cmd/soundtouch-cli/cmd_discover.go index 690b337..fc260eb 100644 --- a/cmd/soundtouch-cli/cmd_discover.go +++ b/cmd/soundtouch-cli/cmd_discover.go @@ -16,10 +16,12 @@ func discoverDevices(c *cli.Context) error { 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 @@ -35,6 +37,7 @@ func discoverDevices(c *cli.Context) error { // Create discovery service discoveryService := discovery.NewUnifiedDiscoveryService(cfg) + ctx, cancel := context.WithTimeout(context.Background(), cfg.DiscoveryTimeout+5*time.Second) defer cancel() @@ -52,6 +55,7 @@ func discoverDevices(c *cli.Context) error { fmt.Println("- Devices are on a different network segment") fmt.Println("- Network blocks multicast traffic") fmt.Println("- Firewall is blocking discovery ports") + return nil } diff --git a/cmd/soundtouch-cli/cmd_info.go b/cmd/soundtouch-cli/cmd_info.go index 846e390..51ffb21 100644 --- a/cmd/soundtouch-cli/cmd_info.go +++ b/cmd/soundtouch-cli/cmd_info.go @@ -10,6 +10,7 @@ import ( // getDeviceInfo handles the device info command func getDeviceInfo(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -27,6 +28,7 @@ func getDeviceInfo(c *cli.Context) error { 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) } @@ -46,9 +48,11 @@ func getDeviceInfo(c *cli.Context) error { 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) } @@ -61,6 +65,7 @@ func getDeviceInfo(c *cli.Context) error { // getDeviceName handles getting the device name func getDeviceName(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -74,12 +79,14 @@ func getDeviceName(c *cli.Context) error { } 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 @@ -98,12 +105,14 @@ func setDeviceName(c *cli.Context) error { } 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 @@ -137,9 +146,11 @@ func getCapabilities(c *cli.Context) error { for _, capName := range capNames { capability := capabilities.GetCapabilityByName(capName) fmt.Printf(" - %s", capName) + if capability.URL != "" { fmt.Printf(" (%s)", capability.URL) } + fmt.Println() } } @@ -150,6 +161,7 @@ func getCapabilities(c *cli.Context) error { // getPresets handles getting device presets func getPresets(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -170,6 +182,7 @@ func getPresets(c *cli.Context) error { } 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) @@ -213,6 +226,7 @@ func selectPreset(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Preset %d selected", presetNum)) + return nil } diff --git a/cmd/soundtouch-cli/cmd_network.go b/cmd/soundtouch-cli/cmd_network.go index e85f05f..d2b4178 100644 --- a/cmd/soundtouch-cli/cmd_network.go +++ b/cmd/soundtouch-cli/cmd_network.go @@ -36,7 +36,9 @@ func getNetworkInfo(c *cli.Context) error { } fmt.Printf(" Interfaces (%d):\n", len(interfaces)) - for i, iface := range interfaces { + + for i := range interfaces { + iface := &interfaces[i] fmt.Printf("\n Interface %d:\n", i+1) fmt.Printf(" Type: %s\n", iface.GetType()) @@ -77,7 +79,9 @@ func getNetworkInfo(c *cli.Context) error { activeInterfaces := networkInfo.GetActiveInterfaces() if len(activeInterfaces) > 0 { fmt.Println("\n Active Connections:") - for _, iface := range activeInterfaces { + + for i := range activeInterfaces { + iface := &activeInterfaces[i] fmt.Printf(" - %s: %s\n", iface.GetType(), iface.GetNetworkSummary()) } } @@ -103,6 +107,7 @@ func pingDevice(c *cli.Context) error { } PrintSuccess("Device is reachable") + return nil } @@ -118,5 +123,6 @@ func getDeviceURL(c *cli.Context) error { baseURL := client.BaseURL() fmt.Printf("Device URL: %s\n", baseURL) + return nil } diff --git a/cmd/soundtouch-cli/cmd_playback.go b/cmd/soundtouch-cli/cmd_playback.go index 6ae0e19..8270487 100644 --- a/cmd/soundtouch-cli/cmd_playback.go +++ b/cmd/soundtouch-cli/cmd_playback.go @@ -11,6 +11,7 @@ import ( // 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 @@ -48,6 +49,7 @@ func getNowPlaying(c *cli.Context) error { if nowPlaying.HasTimeInfo() { fmt.Printf(" Duration: %s\n", nowPlaying.FormatDuration()) + if nowPlaying.Position != nil { fmt.Printf(" Position: %s\n", nowPlaying.FormatPosition()) } @@ -67,6 +69,7 @@ func getNowPlaying(c *cli.Context) error { // playCommand handles play command func playCommand(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -80,12 +83,14 @@ func playCommand(c *cli.Context) error { } 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 @@ -99,12 +104,14 @@ func pauseCommand(c *cli.Context) error { } 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 @@ -118,12 +125,14 @@ func stopCommand(c *cli.Context) error { } 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 @@ -137,12 +146,14 @@ func nextCommand(c *cli.Context) error { } 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 @@ -156,6 +167,7 @@ func prevCommand(c *cli.Context) error { } PrintSuccess("Previous track command sent") + return nil } @@ -178,6 +190,7 @@ func sendKey(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("%s key command sent", key)) + return nil } @@ -199,6 +212,7 @@ func powerCommand(c *cli.Context) error { } PrintSuccess("Power command sent") + return nil } @@ -220,6 +234,7 @@ func muteCommand(c *cli.Context) error { } PrintSuccess("Mute command sent") + return nil } @@ -241,6 +256,7 @@ func thumbsUpCommand(c *cli.Context) error { } PrintSuccess("Thumbs up command sent") + return nil } @@ -262,6 +278,7 @@ func thumbsDownCommand(c *cli.Context) error { } PrintSuccess("Thumbs down command sent") + return nil } @@ -283,6 +300,7 @@ func volumeUpKey(c *cli.Context) error { } PrintSuccess("Volume up command sent") + return nil } @@ -304,5 +322,6 @@ func volumeDownKey(c *cli.Context) error { } PrintSuccess("Volume down command sent") + return nil } diff --git a/cmd/soundtouch-cli/cmd_source.go b/cmd/soundtouch-cli/cmd_source.go index 5e42750..b5e7181 100644 --- a/cmd/soundtouch-cli/cmd_source.go +++ b/cmd/soundtouch-cli/cmd_source.go @@ -10,6 +10,7 @@ import ( // listSources handles listing available audio sources func listSources(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -32,6 +33,7 @@ func listSources(c *cli.Context) error { for _, source := range availableSources { fmt.Printf(" • %s", source.GetDisplayName()) + if source.SourceAccount != "" && source.SourceAccount != source.Source { fmt.Printf(" (%s)", source.SourceAccount) } @@ -40,18 +42,22 @@ func listSources(c *cli.Context) error { 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() { @@ -59,6 +65,7 @@ func listSources(c *cli.Context) error { } fmt.Printf(" • %s (%s)\n", source.GetDisplayName(), status) + if source.SourceAccount != "" && source.SourceAccount != source.Source { fmt.Printf(" Account: %s\n", source.SourceAccount) } @@ -68,11 +75,14 @@ func listSources(c *cli.Context) error { 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() } } @@ -83,6 +93,7 @@ func listSources(c *cli.Context) error { // selectSource handles selecting an audio source func selectSource(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -110,6 +121,7 @@ func selectSource(c *cli.Context) error { // selectSpotify handles selecting Spotify source func selectSpotify(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -123,12 +135,14 @@ func selectSpotify(c *cli.Context) error { } 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 @@ -142,12 +156,14 @@ func selectBluetooth(c *cli.Context) error { } 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 @@ -161,5 +177,6 @@ func selectAux(c *cli.Context) error { } PrintSuccess("AUX input source selected") + return nil } diff --git a/cmd/soundtouch-cli/cmd_volume.go b/cmd/soundtouch-cli/cmd_volume.go index 3c8bf63..b77fcf7 100644 --- a/cmd/soundtouch-cli/cmd_volume.go +++ b/cmd/soundtouch-cli/cmd_volume.go @@ -11,6 +11,7 @@ import ( // 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 @@ -39,6 +40,7 @@ func getVolume(c *cli.Context) error { // setVolume handles setting the volume level func setVolume(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -77,6 +79,7 @@ func setVolume(c *cli.Context) error { // volumeUp handles increasing the volume func volumeUp(c *cli.Context) error { clientConfig := GetClientConfig(c) + client, err := CreateSoundTouchClient(clientConfig) if err != nil { return err @@ -95,12 +98,14 @@ func volumeUp(c *cli.Context) error { } 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 @@ -119,5 +124,6 @@ func volumeDown(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Volume decreased to %d (%s)", volume.GetLevel(), models.GetVolumeLevelName(volume.GetLevel()))) + return nil } diff --git a/cmd/soundtouch-cli/cmd_zone.go b/cmd/soundtouch-cli/cmd_zone.go index f08f29d..50f65bd 100644 --- a/cmd/soundtouch-cli/cmd_zone.go +++ b/cmd/soundtouch-cli/cmd_zone.go @@ -36,11 +36,14 @@ func getZone(c *cli.Context) error { if len(zone.Members) > 0 { fmt.Printf(" Members (%d):\n", len(zone.Members)) + for _, member := range zone.Members { fmt.Printf(" - %s", member.DeviceID) + if member.IP != "" { fmt.Printf(" (IP: %s)", member.IP) } + fmt.Println() } } else { @@ -102,11 +105,14 @@ func getZoneMembers(c *cli.Context) error { } fmt.Printf("Zone Members (%d):\n", len(members)) + for i, member := range members { fmt.Printf(" %d. %s", i+1, member) + if member == clientConfig.Host { fmt.Print(" (this device)") } + fmt.Println() } @@ -133,12 +139,14 @@ func createZone(c *cli.Context) error { // Parse and validate member IPs var memberIPs []net.IP + for _, member := range members { ip := net.ParseIP(member) if ip == nil { PrintError(fmt.Sprintf("Invalid IP address: %s", member)) return fmt.Errorf("invalid IP address: %s", member) } + memberIPs = append(memberIPs, ip) } @@ -146,6 +154,7 @@ func createZone(c *cli.Context) error { // In a real scenario, you might want to specify the master separately masterDeviceID := "master" // This would need to be a real device ID memberMap := make(map[string]string) + for i, ip := range memberIPs { deviceID := fmt.Sprintf("device_%d", i+1) memberMap[deviceID] = ip.String() @@ -158,6 +167,7 @@ func createZone(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Zone created with members: %s", strings.Join(members, ", "))) + return nil } @@ -189,6 +199,7 @@ func addToZone(c *cli.Context) error { // For this example, we'll use the IP as the device ID // In practice, you'd need the actual device ID deviceID := memberIP + err = client.AddToZone(deviceID, memberIP) if err != nil { PrintError(fmt.Sprintf("Failed to add to zone: %v", err)) @@ -196,6 +207,7 @@ func addToZone(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Added %s to zone", memberIP)) + return nil } @@ -227,6 +239,7 @@ func removeFromZone(c *cli.Context) error { // For this example, we'll use the IP as the device ID // In practice, you'd need the actual device ID deviceID := memberIP + err = client.RemoveFromZone(deviceID) if err != nil { PrintError(fmt.Sprintf("Failed to remove from zone: %v", err)) @@ -234,6 +247,7 @@ func removeFromZone(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Removed %s from zone", memberIP)) + return nil } @@ -255,6 +269,7 @@ func dissolveZone(c *cli.Context) error { } PrintSuccess("Zone dissolved") + return nil } diff --git a/cmd/soundtouch-cli/common.go b/cmd/soundtouch-cli/common.go index f921203..ab1cfad 100644 --- a/cmd/soundtouch-cli/common.go +++ b/cmd/soundtouch-cli/common.go @@ -66,6 +66,7 @@ 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 } diff --git a/pkg/discovery/unified.go b/pkg/discovery/unified.go index f5057d8..974987d 100644 --- a/pkg/discovery/unified.go +++ b/pkg/discovery/unified.go @@ -12,7 +12,7 @@ import ( // UnifiedDiscoveryService combines SSDP and mDNS discovery methods type UnifiedDiscoveryService struct { - ssdpService *DiscoveryService + ssdpService *Service mdnsService *MDNSDiscoveryService config *config.Config cache map[string]*models.DiscoveredDevice @@ -33,7 +33,7 @@ func NewUnifiedDiscoveryService(cfg *config.Config) *UnifiedDiscoveryService { } return &UnifiedDiscoveryService{ - ssdpService: NewDiscoveryServiceWithConfig(cfg), + ssdpService: NewServiceWithConfig(cfg), mdnsService: NewMDNSDiscoveryService(timeout), config: cfg, cache: make(map[string]*models.DiscoveredDevice), diff --git a/pkg/discovery/upnp.go b/pkg/discovery/upnp.go index 55a59c2..58c6798 100644 --- a/pkg/discovery/upnp.go +++ b/pkg/discovery/upnp.go @@ -16,8 +16,8 @@ import ( "github.com/user_account/bose-soundtouch/pkg/models" ) -// DiscoveryService handles UPnP SSDP discovery of SoundTouch devices -type DiscoveryService struct { +// Service handles UPnP SSDP discovery of SoundTouch devices +type Service struct { timeout time.Duration cache map[string]*models.DiscoveredDevice cacheTTL time.Duration @@ -25,13 +25,13 @@ type DiscoveryService struct { config *config.Config } -// NewDiscoveryService creates a new UPnP discovery service -func NewDiscoveryService(timeout time.Duration) *DiscoveryService { +// NewService creates a new UPnP discovery service +func NewService(timeout time.Duration) *Service { if timeout == 0 { timeout = defaultTimeout } - return &DiscoveryService{ + return &Service{ timeout: timeout, cache: make(map[string]*models.DiscoveredDevice), cacheTTL: defaultCacheTTL, @@ -40,8 +40,8 @@ func NewDiscoveryService(timeout time.Duration) *DiscoveryService { } } -// NewDiscoveryServiceWithConfig creates a new discovery service with configuration -func NewDiscoveryServiceWithConfig(cfg *config.Config) *DiscoveryService { +// NewServiceWithConfig creates a new discovery service with configuration +func NewServiceWithConfig(cfg *config.Config) *Service { timeout := cfg.DiscoveryTimeout if timeout == 0 { timeout = defaultTimeout @@ -52,7 +52,7 @@ func NewDiscoveryServiceWithConfig(cfg *config.Config) *DiscoveryService { cacheTTL = defaultCacheTTL } - return &DiscoveryService{ + return &Service{ timeout: timeout, cache: make(map[string]*models.DiscoveredDevice), cacheTTL: cacheTTL, @@ -62,7 +62,7 @@ func NewDiscoveryServiceWithConfig(cfg *config.Config) *DiscoveryService { } // DiscoverDevices discovers all SoundTouch devices on the network -func (d *DiscoveryService) DiscoverDevices(ctx context.Context) ([]*models.DiscoveredDevice, error) { +func (d *Service) DiscoverDevices(ctx context.Context) ([]*models.DiscoveredDevice, error) { // Check cache first d.cleanupCache() @@ -97,7 +97,7 @@ func (d *DiscoveryService) DiscoverDevices(ctx context.Context) ([]*models.Disco } // DiscoverDevice discovers a specific SoundTouch device by host -func (d *DiscoveryService) DiscoverDevice(ctx context.Context, host string) (*models.DiscoveredDevice, error) { +func (d *Service) DiscoverDevice(ctx context.Context, host string) (*models.DiscoveredDevice, error) { // Check cache first d.mutex.RLock() @@ -124,13 +124,13 @@ func (d *DiscoveryService) DiscoverDevice(ctx context.Context, host string) (*mo } // GetCachedDevices returns all cached devices that haven't expired -func (d *DiscoveryService) GetCachedDevices() []*models.DiscoveredDevice { +func (d *Service) GetCachedDevices() []*models.DiscoveredDevice { d.cleanupCache() return d.getCachedDevices() } // ClearCache clears the device cache -func (d *DiscoveryService) ClearCache() { +func (d *Service) ClearCache() { d.mutex.Lock() defer d.mutex.Unlock() @@ -138,7 +138,7 @@ func (d *DiscoveryService) ClearCache() { } // performDiscovery performs the actual UPnP SSDP discovery -func (d *DiscoveryService) performDiscovery(ctx context.Context) ([]*models.DiscoveredDevice, error) { +func (d *Service) performDiscovery(ctx context.Context) ([]*models.DiscoveredDevice, error) { log.Printf("UPnP: Starting SSDP discovery for '%s' with timeout %v", soundTouchURN, d.timeout) // Create UDP connection for multicast @@ -235,7 +235,7 @@ func (d *DiscoveryService) performDiscovery(ctx context.Context) ([]*models.Disc } // buildMSearchRequest builds the M-SEARCH request for SoundTouch devices -func (d *DiscoveryService) buildMSearchRequest() string { +func (d *Service) buildMSearchRequest() string { return fmt.Sprintf( "M-SEARCH * HTTP/1.1\r\n"+ "HOST: %s\r\n"+ @@ -250,7 +250,7 @@ func (d *DiscoveryService) buildMSearchRequest() string { } // parseResponse parses UPnP SSDP response and extracts device information -func (d *DiscoveryService) parseResponse(response string) (*models.DiscoveredDevice, error) { +func (d *Service) parseResponse(response string) (*models.DiscoveredDevice, error) { log.Printf("UPnP: Parsing response (%d chars): %.100s...", len(response), strings.ReplaceAll(response, "\r\n", "\\r\\n")) // Try both \r\n and \n line endings @@ -338,7 +338,7 @@ func (d *DiscoveryService) parseResponse(response string) (*models.DiscoveredDev } // parseLocationURL extracts basic device info from the location URL -func (d *DiscoveryService) parseLocationURL(location string) (*models.DiscoveredDevice, error) { +func (d *Service) parseLocationURL(location string) (*models.DiscoveredDevice, error) { log.Printf("UPnP: Parsing location URL: %s", location) // Parse the URL to extract host and port @@ -366,7 +366,7 @@ func (d *DiscoveryService) parseLocationURL(location string) (*models.Discovered } // enrichDeviceInfo tries to get additional device information from the device description -func (d *DiscoveryService) enrichDeviceInfo(_ *models.DiscoveredDevice, location string) error { +func (d *Service) enrichDeviceInfo(_ *models.DiscoveredDevice, location string) error { log.Printf("UPnP: Attempting to enrich device info by fetching %s", location) client := &http.Client{ @@ -391,7 +391,7 @@ func (d *DiscoveryService) enrichDeviceInfo(_ *models.DiscoveredDevice, location } // updateCache updates the device cache with discovered devices -func (d *DiscoveryService) updateCache(devices []*models.DiscoveredDevice) { +func (d *Service) updateCache(devices []*models.DiscoveredDevice) { d.mutex.Lock() defer d.mutex.Unlock() @@ -401,7 +401,7 @@ func (d *DiscoveryService) updateCache(devices []*models.DiscoveredDevice) { } // getCachedDevices returns all valid cached devices (internal method) -func (d *DiscoveryService) getCachedDevices() []*models.DiscoveredDevice { +func (d *Service) getCachedDevices() []*models.DiscoveredDevice { d.mutex.RLock() defer d.mutex.RUnlock() @@ -416,7 +416,7 @@ func (d *DiscoveryService) getCachedDevices() []*models.DiscoveredDevice { } // cleanupCache removes expired devices from cache -func (d *DiscoveryService) cleanupCache() { +func (d *Service) cleanupCache() { d.mutex.Lock() defer d.mutex.Unlock() @@ -428,12 +428,12 @@ func (d *DiscoveryService) cleanupCache() { } // getConfiguredDevices returns devices from configuration -func (d *DiscoveryService) getConfiguredDevices() []*models.DiscoveredDevice { +func (d *Service) getConfiguredDevices() []*models.DiscoveredDevice { return d.config.GetPreferredDevicesAsDiscovered() } // mergeDevices merges two device lists, avoiding duplicates based on host -func (d *DiscoveryService) mergeDevices(existing, newDevices []*models.DiscoveredDevice) []*models.DiscoveredDevice { +func (d *Service) mergeDevices(existing, newDevices []*models.DiscoveredDevice) []*models.DiscoveredDevice { hostSet := make(map[string]bool) result := make([]*models.DiscoveredDevice, 0, len(existing)+len(newDevices)) diff --git a/pkg/discovery/upnp_test.go b/pkg/discovery/upnp_test.go index 202718f..2f59b6f 100644 --- a/pkg/discovery/upnp_test.go +++ b/pkg/discovery/upnp_test.go @@ -12,7 +12,7 @@ import ( func TestNewDiscoveryService(t *testing.T) { timeout := 5 * time.Second - service := NewDiscoveryService(timeout) + service := NewService(timeout) if service.timeout != timeout { t.Errorf("Expected timeout %v, got %v", timeout, service.timeout) @@ -28,7 +28,7 @@ func TestNewDiscoveryService(t *testing.T) { } func TestNewDiscoveryServiceWithDefaultTimeout(t *testing.T) { - service := NewDiscoveryService(0) + service := NewService(0) if service.timeout != defaultTimeout { t.Errorf("Expected default timeout %v, got %v", defaultTimeout, service.timeout) @@ -36,7 +36,7 @@ func TestNewDiscoveryServiceWithDefaultTimeout(t *testing.T) { } func TestBuildMSearchRequest(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(5 * time.Second) request := service.buildMSearchRequest() expectedLines := []string{ @@ -60,7 +60,7 @@ func TestBuildMSearchRequest(t *testing.T) { } func TestParseLocationURL_Valid(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) location := "http://192.168.1.100:8090/device.xml" device, err := service.parseLocationURL(location) @@ -91,7 +91,7 @@ func TestParseLocationURL_Valid(t *testing.T) { } func TestParseLocationURL_Invalid(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) invalidURLs := []string{ "not-a-url", @@ -109,7 +109,7 @@ func TestParseLocationURL_Invalid(t *testing.T) { } func TestParseResponse_ValidMediaRenderer(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) validResponse := `HTTP/1.1 200 OK Cache-Control: max-age=1800 @@ -141,7 +141,7 @@ USN: uuid:12345678-1234-5678-9012-123456789012::urn:schemas-upnp-org:device:Medi } func TestParseResponse_NotMediaRenderer(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) nonMediaRendererResponse := `HTTP/1.1 200 OK Cache-Control: max-age=1800 @@ -170,7 +170,7 @@ USN: uuid:12345678-1234-5678-9012-123456789012::urn:schemas-upnp-org:device:Some } func TestParseResponse_InvalidHTTP(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) invalidResponses := []string{ "not http response", @@ -192,7 +192,7 @@ func TestParseResponse_InvalidHTTP(t *testing.T) { } func TestParseResponse_NoLocation(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) responseWithoutLocation := `HTTP/1.1 200 OK Cache-Control: max-age=1800 @@ -217,7 +217,7 @@ ST: urn:schemas-upnp-org:device:MediaRenderer:1 } func TestCacheOperations(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(1 * time.Second) // Test empty cache devices := service.GetCachedDevices() @@ -259,7 +259,7 @@ func TestCacheOperations(t *testing.T) { } func TestCacheExpiration(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(5 * time.Second) service.cacheTTL = 100 * time.Millisecond // Short TTL for testing // Add device with old timestamp @@ -295,7 +295,7 @@ func TestCacheExpiration(t *testing.T) { } func TestDiscoverDevices_UseCache(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(5 * time.Second) // Add fresh device to cache freshDevice := &models.DiscoveredDevice{ @@ -326,7 +326,7 @@ func TestDiscoverDevices_UseCache(t *testing.T) { } func TestDiscoverDevice_FromCache(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(5 * time.Second) // Add device to cache device := &models.DiscoveredDevice{ @@ -357,7 +357,7 @@ func TestDiscoverDevice_FromCache(t *testing.T) { } func TestDiscoverDevice_NotFound(t *testing.T) { - service := NewDiscoveryService(100 * time.Millisecond) // Short timeout + service := NewService(100 * time.Millisecond) // Short timeout ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() @@ -384,7 +384,7 @@ func TestNewDiscoveryServiceWithConfig(t *testing.T) { }, } - service := NewDiscoveryServiceWithConfig(cfg) + service := NewServiceWithConfig(cfg) if service.timeout != 15*time.Second { t.Errorf("Expected timeout 15s, got %v", service.timeout) @@ -407,7 +407,7 @@ func TestGetConfiguredDevices(t *testing.T) { }, } - service := NewDiscoveryServiceWithConfig(cfg) + service := NewServiceWithConfig(cfg) devices := service.getConfiguredDevices() if len(devices) != 2 { @@ -428,7 +428,7 @@ func TestGetConfiguredDevices(t *testing.T) { } func TestMergeDevices(t *testing.T) { - service := NewDiscoveryService(5 * time.Second) + service := NewService(5 * time.Second) existing := []*models.DiscoveredDevice{ {Host: "192.168.1.100", Name: "Device 1", Port: 8090}, @@ -475,7 +475,7 @@ func TestDiscoverDevices_ConfiguredOnly(t *testing.T) { }, } - service := NewDiscoveryServiceWithConfig(cfg) + service := NewServiceWithConfig(cfg) ctx := context.Background() devices, err := service.DiscoverDevices(ctx)