From 7015e04556f78f3bd031a801a2b09a10f415afc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lipinsk=C3=BD?= <6032558+Mr-Tao@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:01:48 +0200 Subject: [PATCH] fix(player): retry configured devices during discovery --- cmd/soundtouch-player/README.md | 4 +- cmd/soundtouch-player/main.go | 6 +-- pkg/service/soundtouchweb/discovery.go | 21 +++++++- pkg/service/soundtouchweb/discovery_test.go | 58 +++++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 pkg/service/soundtouchweb/discovery_test.go diff --git a/cmd/soundtouch-player/README.md b/cmd/soundtouch-player/README.md index 499e2383..b2f0a947 100644 --- a/cmd/soundtouch-player/README.md +++ b/cmd/soundtouch-player/README.md @@ -88,7 +88,7 @@ go build -o soundtouch-player ./soundtouch-player -port 8888 # Connect to specific device -./soundtouch-player -host 192.0.2.100 +./soundtouch-player --devices 192.0.2.100 ``` ### Command Line Options @@ -137,7 +137,7 @@ device datastore). The application automatically discovers SoundTouch devices using: - **mDNS discovery** for local network devices - **UPnP/SSDP discovery** as fallback -- **Manual device addition** via IP address +- **Configured devices** via `--devices`, retried whenever discovery runs ### Real-time Updates The interface maintains WebSocket connections to each device for instant updates of: diff --git a/cmd/soundtouch-player/main.go b/cmd/soundtouch-player/main.go index 1f827bfa..e9da4a2f 100644 --- a/cmd/soundtouch-player/main.go +++ b/cmd/soundtouch-player/main.go @@ -161,7 +161,7 @@ func main() { log.Printf("Trusting AfterTouch service CA from %s", sanitizeLog(caPath)) } - discoveryService := soundtouchweb.NewDiscoveryService(ifaceName) + discoveryService := soundtouchweb.NewDiscoveryService(ifaceName, manualHosts...) // Discover devices on startup go func() { @@ -170,10 +170,6 @@ func main() { webApp.BroadcastDiscoveryStatus("starting", webApp.DeviceCount()) - for _, host := range manualHosts { - webApp.AddDeviceByHost(host, 8090, "manual") - } - webApp.DiscoverDevices(ctx, discoveryService) webApp.BroadcastDiscoveryStatus("completed", webApp.DeviceCount()) diff --git a/pkg/service/soundtouchweb/discovery.go b/pkg/service/soundtouchweb/discovery.go index e7df40bd..3f84310f 100644 --- a/pkg/service/soundtouchweb/discovery.go +++ b/pkg/service/soundtouchweb/discovery.go @@ -10,12 +10,13 @@ import ( "github.com/gesellix/bose-soundtouch/pkg/config" "github.com/gesellix/bose-soundtouch/pkg/discovery" "github.com/gesellix/bose-soundtouch/pkg/service/soundtouchweb/webtypes" + "github.com/gesellix/bose-soundtouch/pkg/speaker" ) // NewDiscoveryService loads config and returns a unified discovery service // preconfigured for the web UI's use (10 s discovery timeout, cache on). // When discoveryInterface is non-empty, mDNS/UPnP are pinned to that NIC. -func NewDiscoveryService(discoveryInterface string) *discovery.UnifiedDiscoveryService { +func NewDiscoveryService(discoveryInterface string, configuredHosts ...string) *discovery.UnifiedDiscoveryService { cfg, err := config.LoadFromEnv() if err != nil { log.Printf("Failed to load config: %v, using defaults", err) @@ -30,6 +31,17 @@ func NewDiscoveryService(discoveryInterface string) *discovery.UnifiedDiscoveryS cfg.DiscoveryInterface = discoveryInterface } + for _, host := range configuredHosts { + if host == "" { + continue + } + + cfg.PreferredDevices = append(cfg.PreferredDevices, config.DeviceConfig{ + Host: host, + Port: speaker.HTTPPort, + }) + } + return discovery.NewUnifiedDiscoveryService(cfg) } @@ -164,6 +176,11 @@ func (app *WebApp) DiscoverDevices(ctx context.Context, discoveryService *discov log.Printf("Found %d devices", len(devices)) for _, device := range devices { - app.AddDeviceByHost(device.Host, device.Port, "discovered") + source := "discovered" + if device.DiscoveryMethod == "Configuration" { + source = "manual" + } + + app.AddDeviceByHost(device.Host, device.Port, source) } } diff --git a/pkg/service/soundtouchweb/discovery_test.go b/pkg/service/soundtouchweb/discovery_test.go new file mode 100644 index 00000000..7c3286bc --- /dev/null +++ b/pkg/service/soundtouchweb/discovery_test.go @@ -0,0 +1,58 @@ +package soundtouchweb + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" +) + +func TestDiscoverDevicesRetriesConfiguredHosts(t *testing.T) { + var available atomic.Bool + var infoRequests atomic.Int32 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/info" { + http.NotFound(w, r) + return + } + + infoRequests.Add(1) + if !available.Load() { + http.Error(w, "offline", http.StatusServiceUnavailable) + return + } + + w.Header().Set("Content-Type", "application/xml") + _, _ = w.Write([]byte(`Configured speakerSoundTouch 10`)) + })) + defer server.Close() + + t.Setenv("UPNP_ENABLED", "false") + t.Setenv("MDNS_ENABLED", "false") + t.Setenv("PREFERRED_DEVICES", "") + + configuredHost := strings.TrimPrefix(server.URL, "http://") + discoveryService := NewDiscoveryService("", configuredHost) + app := NewWebApp() + + app.DiscoverDevices(context.Background(), discoveryService) + if got := app.DeviceCount(); got != 0 { + t.Fatalf("device count after offline probe = %d, want 0", got) + } + + available.Store(true) + app.DiscoverDevices(context.Background(), discoveryService) + if got := app.DeviceCount(); got != 1 { + t.Fatalf("device count after retry = %d, want 1", got) + } + if got := infoRequests.Load(); got != 2 { + t.Fatalf("/info request count = %d, want 2", got) + } + + if !app.RemoveDevice(configuredHost) { + t.Fatal("configured device was not registered under its host") + } +}