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.
This commit is contained in:
Tobias Gesellchen
2026-08-17 22:06:31 +02:00
parent df7b0fc7ae
commit d96f564847
5 changed files with 81 additions and 45 deletions
+11
View File
@@ -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()
+13
View File
@@ -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;
+46 -44
View File
@@ -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, "", " ")
+9
View File
@@ -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
}
+2 -1
View File
@@ -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)
}