From d96f56484795a1ed1ac17083bb5e2afbe48917e4 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Mon, 17 Aug 2026 22:06:31 +0200 Subject: [PATCH] feat(soundtouchweb): make #622 auto-resume opt-in via settings.json Automatically re-triggering content selection without a user action isn't something every operator wants, and we haven't independently confirmed the root cause generalises beyond the original report. Add Settings.AutoResumeOnSourceDisconnect (default false, hand-edit settings.json to enable, matching the TuneInStreamFormats precedent - no admin UI control yet). Wired through a WebApp hook so the standalone soundtouch-player build stays unaffected, and read fresh per drop so toggling the setting takes effect without a restart. --- cmd/soundtouch-service/main.go | 11 +++ pkg/service/datastore/datastore.go | 13 ++++ pkg/service/handlers/handlers_export.go | 90 +++++++++++++------------ pkg/service/soundtouchweb/handler.go | 9 +++ pkg/service/soundtouchweb/websocket.go | 3 +- 5 files changed, 81 insertions(+), 45 deletions(-) diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index 849593d..dfc2d1f 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -1471,6 +1471,17 @@ func newEmbeddedWebApp(server *handlers.Server, serverURL, internalURL string, d return err } + // Opt-in (#622): hand-edit settings.json's auto_resume_on_source_disconnect + // to enable. Read fresh per drop so toggling it applies without a restart. + webApp.AutoResumeOnSourceDisconnect = func() bool { + settings, err := ds.GetSettings() + if err != nil { + return false + } + + return settings.AutoResumeOnSourceDisconnect + } + // Keep the UI registry live as the service discovers or devices are added. server.SetDevicesChangedHook(func() { webApp.SeedExtraDevices() diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go index 3f1bb55..52dccdd 100644 --- a/pkg/service/datastore/datastore.go +++ b/pkg/service/datastore/datastore.go @@ -2651,6 +2651,19 @@ type Settings struct { // individual format tokens. TuneInStreamFormats string `json:"tunein_stream_formats,omitempty"` + // AutoResumeOnSourceDisconnect, when true, re-issues a device's last + // playing content item if now_playing drops into an error source right + // after a healthy one, instead of leaving the speaker silent until a + // user manually re-selects it. See #622: some TuneIn streams disconnect + // the speaker's own audio pipeline (errorUpdate 1041 + // SOURCE_DISCONNECTED) on their own, mid-playback, with the SoundTouch + // WebSocket control channel staying healthy throughout; the observed + // fix is exactly what pressing the preset again does. Opt-in (default + // false): this automatically re-triggers content selection without a + // user action, which not every operator wants. Hand-edit settings.json + // to enable — no admin UI control yet, matching TuneInStreamFormats. + AutoResumeOnSourceDisconnect bool `json:"auto_resume_on_source_disconnect,omitempty"` + // DefaultLanding selects what the root path "/" serves to a browser: // "chooser" (or empty) — the neutral landing page that links to the // player and the admin/setup console; diff --git a/pkg/service/handlers/handlers_export.go b/pkg/service/handlers/handlers_export.go index e31cb2a..e141368 100644 --- a/pkg/service/handlers/handlers_export.go +++ b/pkg/service/handlers/handlers_export.go @@ -673,28 +673,29 @@ func (s *Server) addSystemFiles(tw *tar.Writer) { // diagSettings is a copy of datastore.Settings with secrets zeroed out so the // struct can be marshalled into the archive without exposing credentials. type diagSettings struct { - ServerURL string `json:"server_url"` - HTTPSServerURL string `json:"https_server_url,omitempty"` - HTTPSServerURLOverride string `json:"https_server_url_override,omitempty"` - RedactLogs bool `json:"redact_logs"` - LogBodies bool `json:"log_bodies"` - RecordInteractions bool `json:"record_interactions"` - DiscoveryInterval string `json:"discovery_interval,omitempty"` - DiscoveryEnabled bool `json:"discovery_enabled"` - DNSEnabled bool `json:"dns_enabled"` - DNSUpstream []string `json:"dns_upstream,omitempty"` - DNSBindAddr string `json:"dns_bind_addr,omitempty"` - InternalPaths []string `json:"internal_paths,omitempty"` - Shortcuts map[string]int `json:"shortcuts,omitempty"` - SpotifyClientID string `json:"spotify_client_id,omitempty"` - SpotifyClientSecret string `json:"spotify_client_secret,omitempty"` - SpotifyRedirectURI string `json:"spotify_redirect_uri,omitempty"` - AmazonClientID string `json:"amazon_client_id,omitempty"` - AmazonClientSecret string `json:"amazon_client_secret,omitempty"` - AmazonRedirectURI string `json:"amazon_redirect_uri,omitempty"` - TrustForwardedHeaders bool `json:"trust_forwarded_headers,omitempty"` - TrustedProxyCIDRs []string `json:"trusted_proxy_cidrs,omitempty"` - TuneInStreamFormats string `json:"tunein_stream_formats,omitempty"` + ServerURL string `json:"server_url"` + HTTPSServerURL string `json:"https_server_url,omitempty"` + HTTPSServerURLOverride string `json:"https_server_url_override,omitempty"` + RedactLogs bool `json:"redact_logs"` + LogBodies bool `json:"log_bodies"` + RecordInteractions bool `json:"record_interactions"` + DiscoveryInterval string `json:"discovery_interval,omitempty"` + DiscoveryEnabled bool `json:"discovery_enabled"` + DNSEnabled bool `json:"dns_enabled"` + DNSUpstream []string `json:"dns_upstream,omitempty"` + DNSBindAddr string `json:"dns_bind_addr,omitempty"` + InternalPaths []string `json:"internal_paths,omitempty"` + Shortcuts map[string]int `json:"shortcuts,omitempty"` + SpotifyClientID string `json:"spotify_client_id,omitempty"` + SpotifyClientSecret string `json:"spotify_client_secret,omitempty"` + SpotifyRedirectURI string `json:"spotify_redirect_uri,omitempty"` + AmazonClientID string `json:"amazon_client_id,omitempty"` + AmazonClientSecret string `json:"amazon_client_secret,omitempty"` + AmazonRedirectURI string `json:"amazon_redirect_uri,omitempty"` + TrustForwardedHeaders bool `json:"trust_forwarded_headers,omitempty"` + TrustedProxyCIDRs []string `json:"trusted_proxy_cidrs,omitempty"` + TuneInStreamFormats string `json:"tunein_stream_formats,omitempty"` + AutoResumeOnSourceDisconnect bool `json:"auto_resume_on_source_disconnect,omitempty"` } // addSettingsJSON serialises the service settings into the archive as @@ -773,28 +774,29 @@ func (s *Server) addSettingsJSON(tw *tar.Writer) { _, effectiveHTTPSURL := s.GetSettings() ds := diagSettings{ - ServerURL: st.ServerURL, - HTTPSServerURL: effectiveHTTPSURL, - HTTPSServerURLOverride: st.HTTPServerURL, - RedactLogs: st.RedactLogs, - LogBodies: st.LogBodies, - RecordInteractions: st.RecordInteractions, - DiscoveryInterval: st.DiscoveryInterval, - DiscoveryEnabled: st.DiscoveryEnabled, - DNSEnabled: st.DNSEnabled, - DNSUpstream: st.DNSUpstream, - DNSBindAddr: st.DNSBindAddr, - InternalPaths: st.InternalPaths, - Shortcuts: st.Shortcuts, - SpotifyClientID: st.SpotifyClientID, - SpotifyClientSecret: redact(st.SpotifyClientSecret), - SpotifyRedirectURI: st.SpotifyRedirectURI, - AmazonClientID: st.AmazonClientID, - AmazonClientSecret: redact(st.AmazonClientSecret), - AmazonRedirectURI: st.AmazonRedirectURI, - TrustForwardedHeaders: st.TrustForwardedHeaders, - TrustedProxyCIDRs: st.TrustedProxyCIDRs, - TuneInStreamFormats: st.TuneInStreamFormats, + ServerURL: st.ServerURL, + HTTPSServerURL: effectiveHTTPSURL, + HTTPSServerURLOverride: st.HTTPServerURL, + RedactLogs: st.RedactLogs, + LogBodies: st.LogBodies, + RecordInteractions: st.RecordInteractions, + DiscoveryInterval: st.DiscoveryInterval, + DiscoveryEnabled: st.DiscoveryEnabled, + DNSEnabled: st.DNSEnabled, + DNSUpstream: st.DNSUpstream, + DNSBindAddr: st.DNSBindAddr, + InternalPaths: st.InternalPaths, + Shortcuts: st.Shortcuts, + SpotifyClientID: st.SpotifyClientID, + SpotifyClientSecret: redact(st.SpotifyClientSecret), + SpotifyRedirectURI: st.SpotifyRedirectURI, + AmazonClientID: st.AmazonClientID, + AmazonClientSecret: redact(st.AmazonClientSecret), + AmazonRedirectURI: st.AmazonRedirectURI, + TrustForwardedHeaders: st.TrustForwardedHeaders, + TrustedProxyCIDRs: st.TrustedProxyCIDRs, + TuneInStreamFormats: st.TuneInStreamFormats, + AutoResumeOnSourceDisconnect: st.AutoResumeOnSourceDisconnect, } data, err := json.MarshalIndent(ds, "", " ") diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index 0868f0b..b652593 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -78,6 +78,15 @@ type WebApp struct { // removal only prunes the in-memory registry). RemoveDeviceHook func(deviceID string) error + // AutoResumeOnSourceDisconnect, when set and returning true, makes + // ConnectDeviceWebSocket re-issue a device's last playing content item + // after an unsolicited drop into an error source (#622). Opt-in: the + // embedded build wires it to Settings.AutoResumeOnSourceDisconnect + // (settings.json, default false); standalone soundtouch-player leaves it + // nil, which disables the behaviour. Read once per drop rather than + // cached, so toggling the setting takes effect without a restart. + AutoResumeOnSourceDisconnect func() bool + discoveryStatus atomic.Value // stores *webtypes.DiscoveryStatus } diff --git a/pkg/service/soundtouchweb/websocket.go b/pkg/service/soundtouchweb/websocket.go index 2afc4cc..46b433e 100644 --- a/pkg/service/soundtouchweb/websocket.go +++ b/pkg/service/soundtouchweb/websocket.go @@ -195,7 +195,8 @@ func (app *WebApp) ConnectDeviceWebSocket(deviceID string, conn *webtypes.Device logNowPlayingError(deviceID, np.Source, np.SourceAccount) } - if item, attempt, shouldResume := resumeState.observe(prevSource, np); shouldResume { + if item, attempt, shouldResume := resumeState.observe(prevSource, np); shouldResume && + app.AutoResumeOnSourceDisconnect != nil && app.AutoResumeOnSourceDisconnect() { go autoResumePlayback(conn, deviceID, item, attempt) }