feat(spotify): improve Spotify registration flow and speaker notification

- Implement full SoundTouch app flow for Spotify registration in the Web UI.
- Update `/mgmt/spotify/init` to pass `accountID` via OAuth `state`.
- Add "Connect Spotify" button to Local Account tab in Web UI with polling.
- Implement legacy and Marge-sync fallbacks for speaker notifications (Error 1029).
- Add support for parsing multi-error XML responses (`<errors>`) from speakers.
- Add `NotifySourcesUpdated` to client for triggering manual source synchronization.
- Improve test coverage for error parsing and Spotify initialization handlers.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
Tobias Gesellchen
2026-04-06 22:39:30 +02:00
co-authored by Junie
parent 4de7911817
commit 276d01fe42
11 changed files with 402 additions and 52 deletions
+7 -1
View File
@@ -141,7 +141,13 @@ type MusicServiceAccountResponse struct {
Status string `xml:",chardata"`
}
// SourcesUpdatedResponse represents the response from /notification
type SourcesUpdatedResponse struct {
XMLName xml.Name `xml:"status"`
Status string `xml:",chardata"`
}
// IsSuccess returns true if the account operation was successful
func (resp *MusicServiceAccountResponse) IsSuccess() bool {
return resp.Status == "/setMusicServiceAccount" || resp.Status == "/removeMusicServiceAccount"
return resp.Status == "/setMusicServiceAccount" || resp.Status == "/removeMusicServiceAccount" || resp.Status == "/notification"
}
+39
View File
@@ -36,6 +36,22 @@ type NetworkInfo struct {
IPAddress string `xml:"ipAddress"`
}
// SourcesUpdatedNotification represents the notification XML sent to the device
type SourcesUpdatedNotification struct {
XMLName xml.Name `xml:"updates"`
DeviceID string `xml:"deviceID,attr"`
Sources struct {
XMLName xml.Name `xml:"sourcesUpdated"`
} `xml:"sourcesUpdated"`
}
// NewSourcesUpdatedNotification creates a new sources updated notification
func NewSourcesUpdatedNotification(deviceID string) *SourcesUpdatedNotification {
return &SourcesUpdatedNotification{
DeviceID: deviceID,
}
}
// XMLResponse is a generic wrapper for API responses
type XMLResponse struct {
XMLName xml.Name
@@ -53,6 +69,29 @@ func (e *APIError) Error() string {
return e.Message
}
// ErrorsResponse represents a multi-error response from the API (common in some firmware versions)
type ErrorsResponse struct {
XMLName xml.Name `xml:"errors"`
DeviceID string `xml:"deviceID,attr"`
Errors []DeviceError `xml:"error"`
}
// Error implements the error interface for ErrorsResponse
func (e *ErrorsResponse) Error() string {
if len(e.Errors) > 0 {
return e.Errors[0].Message
}
return "unknown API error"
}
// DeviceError represents a single error in an ErrorsResponse
type DeviceError struct {
Value int `xml:"value,attr"`
Name string `xml:"name,attr"`
Message string `xml:",chardata"`
}
// DiscoveredDevice represents a device found through network discovery
type DiscoveredDevice struct {
Name string `json:"name"`