fix(player): satisfy projection lint

This commit is contained in:
Lukáš Lipinský
2026-09-05 17:30:23 +02:00
committed by Tobias Gesellchen
parent 60788bfa90
commit b07375d23d
7 changed files with 64 additions and 18 deletions
+19
View File
@@ -236,10 +236,12 @@ func (ws *WebSocketClient) connectWithConfig(config *WebSocketConfig) error {
defer ws.connectMu.Unlock()
ws.mu.RLock()
if ws.connected {
ws.mu.RUnlock()
return fmt.Errorf("already connected")
}
ctx := ws.ctx
dialContext := ws.dialContext
ws.mu.RUnlock()
@@ -288,20 +290,25 @@ func (ws *WebSocketClient) connectWithConfig(config *WebSocketConfig) error {
ws.mu.Lock()
if err := ctx.Err(); err != nil {
ws.mu.Unlock()
_ = conn.Close()
return fmt.Errorf("WebSocket client closed during connect: %w", err)
}
if ws.connected {
ws.mu.Unlock()
_ = conn.Close()
return fmt.Errorf("already connected")
}
connection, transportHandler, transportGeneration := ws.activateConnectionLocked(conn)
ws.mu.Unlock()
notifyTransportState(transportHandler, true, transportGeneration)
go ws.readLoop(config, connection)
go ws.pingLoop(config, connection)
@@ -349,14 +356,17 @@ func (ws *WebSocketClient) Disconnect() error {
ws.mu.Lock()
wasConnected := ws.connected
ws.reconnect = false
if ws.connection != nil {
ws.connection.cancel()
ws.connection = nil
}
conn := ws.conn
ws.conn = nil
ws.connected = false
var (
transportHandler func(bool, uint64)
transportGeneration uint64
@@ -370,13 +380,16 @@ func (ws *WebSocketClient) Disconnect() error {
if conn != nil {
err := conn.Close()
ws.logger.Printf("Disconnected")
notifyTransportState(transportHandler, false, transportGeneration)
return err
}
notifyTransportState(transportHandler, false, transportGeneration)
if !wasConnected {
return fmt.Errorf("not connected")
}
@@ -391,14 +404,17 @@ func (ws *WebSocketClient) Close() error {
ws.mu.Lock()
wasConnected := ws.connected
ws.reconnect = false
if ws.connection != nil {
ws.connection.cancel()
ws.connection = nil
}
conn := ws.conn
ws.conn = nil
ws.connected = false
var (
transportHandler func(bool, uint64)
transportGeneration uint64
@@ -417,6 +433,7 @@ func (ws *WebSocketClient) Close() error {
}
err := conn.Close()
ws.logger.Printf("Disconnected")
notifyTransportState(transportHandler, false, transportGeneration)
@@ -465,7 +482,9 @@ func (ws *WebSocketClient) readLoop(config *WebSocketConfig, connection *webSock
transportGeneration := ws.transportGeneration
reconnect := ws.reconnect
ws.mu.Unlock()
_ = connection.conn.Close()
notifyTransportState(transportHandler, false, transportGeneration)
// Attempt reconnection if enabled
@@ -263,14 +263,16 @@ func newStereoPairView(group *models.Group, byDeviceID map[string][]deviceProjec
func projectedDeviceInfo(controlID string, info *models.DeviceInfo, pair *stereoPairView) *models.DeviceInfo {
if info == nil {
return info
return nil
}
address := projectedIPAddress(controlID, info, "")
name := info.Name
if pair != nil && pair.Name != "" {
name = pair.Name
}
if address == info.IPAddress && name == info.Name {
return info
}
@@ -301,9 +303,11 @@ func projectedConnectivity(status *webtypes.DeviceStatus) webtypes.Connectivity
if status == nil {
return webtypes.ConnectivityOffline
}
if status.Connectivity != "" {
return status.Connectivity
}
if status.IsConnected {
return webtypes.ConnectivityOnline
}
+21 -11
View File
@@ -66,10 +66,15 @@ func NewDiscoveryService(discoveryInterface string, configuredHosts ...string) *
// mDNS/UPnP. If the host is already known, the existing entry's
// LastSeen is bumped and the function returns without re-fetching.
func (app *WebApp) AddDeviceByHost(host string, port int, source string) {
app.addDeviceByHost(host, port, source)
app.addDeviceByHost(context.Background(), host, port, source)
}
func (app *WebApp) addDeviceByHost(host string, port int, source string) *webtypes.DeviceConnection {
func (app *WebApp) addDeviceByHost(
ctx context.Context,
host string,
port int,
source string,
) *webtypes.DeviceConnection {
// Fast path: skip the network call if we already know this host.
if app.TouchDevice(host) {
return nil
@@ -89,10 +94,11 @@ func (app *WebApp) addDeviceByHost(host string, port int, source string) *webtyp
// Keep the registry key stable for controls, but expose a canonical numeric
// address separately for presentation and sorting.
info.IPAddress = resolvedDeviceIPAddress(host, info)
info.IPAddress = resolvedDeviceIPAddress(ctx, host, info)
conn := webtypes.NewDeviceConnection(c, info)
conn.MarkHTTPSuccess(time.Now())
if !app.AddDevice(host, conn) {
// Lost a race — another goroutine inserted the same host
// between TouchDevice and AddDevice. AddDevice bumped LastSeen
@@ -124,7 +130,7 @@ func (app *WebApp) addDeviceByHost(host string, port int, source string) *webtyp
return conn
}
func resolvedDeviceIPAddress(host string, info *models.DeviceInfo) string {
func resolvedDeviceIPAddress(ctx context.Context, host string, info *models.DeviceInfo) string {
if info != nil {
if address := numericIPAddress(info.IPAddress); address != "" {
return address
@@ -142,7 +148,7 @@ func resolvedDeviceIPAddress(host string, info *models.DeviceInfo) string {
return address
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
addresses, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", bareHost)
@@ -178,7 +184,7 @@ func numericIPAddress(address string) string {
// need to distinguish "read failed" from "converged" (the bounded startup
// retry) should call seedExtraDevices directly instead.
func (app *WebApp) SeedExtraDevices() {
if _, _, _, err := app.seedExtraDevices(); err != nil {
if _, _, _, err := app.seedExtraDevices(context.Background()); err != nil {
log.Printf("SeedExtraDevices: failed to read extra device hosts: %v", err)
}
}
@@ -203,7 +209,9 @@ type seededExtraDevice struct {
// A non-nil error means the hook itself failed (e.g. a datastore glitch);
// callers must treat that as "unknown state, don't prune, don't declare
// ready" rather than as an empty desired set.
func (app *WebApp) seedExtraDevices() (inserted []seededExtraDevice, removed int, desired map[string]struct{}, err error) {
func (app *WebApp) seedExtraDevices(
ctx context.Context,
) (inserted []seededExtraDevice, removed int, desired map[string]struct{}, err error) {
if app.ExtraDeviceHosts == nil {
return nil, 0, nil, nil
}
@@ -231,7 +239,7 @@ func (app *WebApp) seedExtraDevices() (inserted []seededExtraDevice, removed int
go func(h string) {
defer wg.Done()
conn := app.addDeviceByHost(h, 8090, "service-store")
conn := app.addDeviceByHost(ctx, h, 8090, "service-store")
if conn == nil {
return
}
@@ -266,7 +274,7 @@ func (app *WebApp) seedExtraDevices() (inserted []seededExtraDevice, removed int
// while the service is starting.
func (app *WebApp) SeedExtraDevicesUntilReady(ctx context.Context, retryInterval time.Duration) {
retryUntilReady(ctx, retryInterval, func() bool {
inserted, removed, desired, err := app.seedExtraDevices()
inserted, removed, desired, err := app.seedExtraDevices(ctx)
if err != nil {
log.Printf("SeedExtraDevicesUntilReady: failed to read extra device hosts, will retry: %v", err)
return false
@@ -373,7 +381,9 @@ func (app *WebApp) DiscoverDevices(ctx context.Context, discoveryService *discov
// Re-sync from the external device source (embedded: the service datastore).
// No-op when ExtraDeviceHosts is unset.
app.SeedExtraDevices()
if _, _, _, err := app.seedExtraDevices(ctx); err != nil {
log.Printf("DiscoverDevices: failed to read extra device hosts: %v", err)
}
// Own mDNS/UPnP sweep — standalone only. The embedded build passes a nil
// discovery service and relies entirely on the host service's discovery.
@@ -394,7 +404,7 @@ 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, classifySource(device.DiscoveryMethod))
app.addDeviceByHost(ctx, device.Host, device.Port, classifySource(device.DiscoveryMethod))
}
}
+2 -2
View File
@@ -107,13 +107,13 @@ func TestResolvedDeviceIPAddressSeparatesHostnameFromReportedAddress(t *testing.
{Type: "SCM", IPAddress: "192.0.2.42"},
}}
if got := resolvedDeviceIPAddress("kitchen.local", info); got != "192.0.2.42" {
if got := resolvedDeviceIPAddress(context.Background(), "kitchen.local", info); got != "192.0.2.42" {
t.Fatalf("resolved address = %q, want reported speaker address", got)
}
}
func TestResolvedDeviceIPAddressPreservesLiteralIP(t *testing.T) {
if got := resolvedDeviceIPAddress("192.0.2.20", &models.DeviceInfo{}); got != "192.0.2.20" {
if got := resolvedDeviceIPAddress(context.Background(), "192.0.2.20", &models.DeviceInfo{}); got != "192.0.2.20" {
t.Fatalf("resolved literal address = %q, want unchanged literal", got)
}
}
+3
View File
@@ -465,6 +465,7 @@ func (app *WebApp) HandleDeleteDevice(w http.ResponseWriter, r *http.Request) {
return
}
app.BroadcastDeviceList()
w.Header().Set("Content-Type", "application/json")
@@ -1057,6 +1058,7 @@ func (app *WebApp) HandleGetZone(w http.ResponseWriter, r *http.Request) {
masterIP := app.findIPByHwID(zone.Master)
masterName := ""
if conn, ok := app.GetDevice(masterIP); ok {
if info := conn.Info(); info != nil {
masterName = info.Name
@@ -1073,6 +1075,7 @@ func (app *WebApp) HandleGetZone(w http.ResponseWriter, r *http.Request) {
for _, m := range zone.Members {
name := ""
if conn, ok := app.GetDevice(m.IP); ok {
if info := conn.Info(); info != nil {
name = info.Name
+3 -4
View File
@@ -492,6 +492,7 @@ func (app *WebApp) ConnectDeviceWebSocket(deviceID string, conn *webtypes.Device
if conn.Client == nil {
return
}
if !conn.TryStartWebSocketLoop() {
return
}
@@ -605,6 +606,7 @@ func (app *WebApp) ConnectDeviceWebSocket(deviceID string, conn *webtypes.Device
if !published {
return
}
if err != nil {
log.Printf("Failed to connect WebSocket for device %s: %v (retrying in %s)", sanitizeLog(deviceID), err, backoff)
@@ -627,10 +629,6 @@ func (app *WebApp) ConnectDeviceWebSocket(deviceID string, conn *webtypes.Device
// disconnected would otherwise stay stale until the next WS event.
go app.UpdateDeviceStatus(deviceID, conn)
// Reset backoff after a successful connect so the next failure
// starts at the lowest cadence again.
backoff = initialBackoff
<-conn.Done()
return
@@ -737,6 +735,7 @@ func (app *WebApp) updateDeviceStatus(_ string, conn *webtypes.DeviceConnection,
if stereoCapable && groupBaseline == nil {
groupGeneration = conn.BeginGroupRefresh()
}
nameGeneration := conn.BeginNameRefresh()
// Phase 1: slow network fetches. Local vars only, no shared state
@@ -126,6 +126,7 @@ type DeviceStatus struct {
// evidence but cannot override a current direct-path success.
type Connectivity string
// Player connectivity states derived from direct and speaker-reported evidence.
const (
ConnectivityOnline Connectivity = "online"
ConnectivityStale Connectivity = "stale"
@@ -179,6 +180,7 @@ func NewDeviceConnection(c *client.Client, info *models.DeviceInfo) *DeviceConne
IsConnected: false,
LastActivity: time.Now(),
})
if info != nil {
conn.storeDeviceName(info.Name)
}
@@ -290,6 +292,7 @@ func (c *DeviceConnection) SetWebSocket(ws *client.WebSocketClient) bool {
select {
case <-c.done:
c.webSocketMu.Unlock()
if ws != nil {
_ = ws.Close()
}
@@ -415,6 +418,7 @@ func (c *DeviceConnection) BeginHTTPPoll() uint64 {
if c.pollEventGen == nil {
c.pollEventGen = make(map[uint64]uint64)
}
c.pollEventGen[c.nextPollGeneration] = c.speakerEventGen
return c.nextPollGeneration
@@ -431,6 +435,7 @@ func (c *DeviceConnection) ApplySpeakerEventAt(at time.Time, mut func(*DeviceSta
if mut != nil {
mut(status)
}
c.applyConnectivityLocked(status, at)
})
}
@@ -481,10 +486,12 @@ func (c *DeviceConnection) ObserveEventStreamTransport(
}
c.lastTransportGeneration = generation
c.eventStreamConnected = connected
if connected {
c.recordDirectSuccessLocked(at)
}
c.UpdateStatus(func(status *DeviceStatus) {
c.applyConnectivityLocked(status, at)
})
@@ -502,6 +509,7 @@ func (c *DeviceConnection) ObserveEventStream(connected bool, at time.Time) {
if connected {
c.recordDirectSuccessLocked(at)
}
c.UpdateStatus(func(status *DeviceStatus) {
c.applyConnectivityLocked(status, at)
})
@@ -550,7 +558,9 @@ func (c *DeviceConnection) CompleteHTTPPoll(
if merge != nil && knownGeneration && pollEventGeneration == c.speakerEventGen {
merge(status)
}
c.applyConnectivityLocked(status, at)
if success {
status.LastActivity = at
}
@@ -601,6 +611,7 @@ func withinConnectivityGrace(at, success time.Time) bool {
if success.IsZero() {
return false
}
if at.Before(success) {
return true
}