mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +00:00
- XML Refactoring: Transitioned from manual string concatenation to structured XML marshaling using specialized Go models to match upstream API responses exactly. - Service Enhancements: Implemented robust device discovery via power_on handling, improved source metadata persistence, and standardized ID generation logic. - Parity & Consistency: Fixed data loss and formatting mismatches for lastplayedat, serialNumber, and nested <source> elements. - Infrastructure & Testing: Added a comprehensive suite of regression and parity reproduction tests, centralized common XML constants, and documented progress. Co-authored-by: Junie <junie@jetbrains.com>
447 lines
20 KiB
Go
447 lines
20 KiB
Go
// Package models defines data structures used for Bose SoundTouch API communication
|
|
// and service management. It includes types for BMX (Bose Media eXchange) services,
|
|
// device information, presets, recents, and other core data models.
|
|
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
)
|
|
|
|
// Link represents a navigational link with URL and client usage preferences.
|
|
type Link struct {
|
|
Href string `json:"href" xml:"href,attr"`
|
|
UseInternalClient string `json:"useInternalClient,omitempty" xml:"useInternalClient,attr,omitempty"`
|
|
}
|
|
|
|
// Links contains various navigation links used by BMX services.
|
|
type Links struct {
|
|
BmxLogout *Link `json:"bmx_logout,omitempty" xml:"bmx_logout,omitempty"`
|
|
BmxNavigate *Link `json:"bmx_navigate,omitempty" xml:"bmx_navigate,omitempty"`
|
|
BmxServicesAvailability *Link `json:"bmx_services_availability,omitempty" xml:"bmx_services_availability,omitempty"`
|
|
BmxToken *Link `json:"bmx_token,omitempty" xml:"bmx_token,omitempty"`
|
|
Self *Link `json:"self,omitempty" xml:"self,omitempty"`
|
|
BmxAvailability *Link `json:"bmx_availability,omitempty" xml:"bmx_availability,omitempty"`
|
|
BmxReporting *Link `json:"bmx_reporting,omitempty" xml:"bmx_reporting,omitempty"`
|
|
BmxFavorite *Link `json:"bmx_favorite,omitempty" xml:"bmx_favorite,omitempty"`
|
|
BmxNowPlaying *Link `json:"bmx_nowplaying,omitempty" xml:"bmx_nowplaying,omitempty"`
|
|
BmxTrack *Link `json:"bmx_track,omitempty" xml:"bmx_track,omitempty"`
|
|
}
|
|
|
|
// IconSet represents a collection of icons with different sizes for media content.
|
|
type IconSet struct {
|
|
DefaultAlbumArt string `json:"defaultAlbumArt,omitempty" xml:"defaultAlbumArt,omitempty"`
|
|
LargeSvg string `json:"largeSvg" xml:"largeSvg"`
|
|
MonochromePng string `json:"monochromePng" xml:"monochromePng"`
|
|
MonochromeSvg string `json:"monochromeSvg" xml:"monochromeSvg"`
|
|
SmallSvg string `json:"smallSvg" xml:"smallSvg"`
|
|
}
|
|
|
|
// Asset represents a media asset with URL and content type information.
|
|
type Asset struct {
|
|
Color string `json:"color" xml:"color"`
|
|
Description string `json:"description" xml:"description"`
|
|
Icons IconSet `json:"icons" xml:"icons"`
|
|
Name string `json:"name" xml:"name"`
|
|
ShortDescription string `json:"shortDescription,omitempty" xml:"shortDescription,omitempty"`
|
|
}
|
|
|
|
// Id represents an identifier structure used in various API responses.
|
|
type Id struct {
|
|
Name string `json:"name" xml:"name"`
|
|
Value int `json:"value" xml:"value"`
|
|
}
|
|
|
|
// BmxService represents a Bose Media eXchange service configuration.
|
|
type BmxService struct {
|
|
Links *Links `json:"_links,omitempty" xml:"links,omitempty"`
|
|
AskAdapter bool `json:"askAdapter" xml:"askAdapter"`
|
|
Assets Asset `json:"assets" xml:"assets"`
|
|
BaseUrl string `json:"baseUrl" xml:"baseUrl"`
|
|
SignupUrl string `json:"signupUrl,omitempty" xml:"signupUrl,omitempty"`
|
|
StreamTypes []string `json:"streamTypes" xml:"streamTypes>streamType"`
|
|
AuthenticationModel map[string]interface{} `json:"authenticationModel" xml:"authenticationModel"`
|
|
ID Id `json:"id" xml:"id"`
|
|
}
|
|
|
|
// BmxResponse represents a response from BMX services.
|
|
type BmxResponse struct {
|
|
Links *Links `json:"_links,omitempty" xml:"links,omitempty"`
|
|
AskAgainAfter int `json:"askAgainAfter" xml:"askAgainAfter"`
|
|
BmxServices []Service `json:"bmx_services" xml:"bmx_services>service"`
|
|
}
|
|
|
|
// Stream represents audio stream information including URL and format details.
|
|
type Stream struct {
|
|
Links *Links `json:"_links,omitempty" xml:"links,omitempty"`
|
|
BufferingTimeout int `json:"bufferingTimeout,omitempty" xml:"bufferingTimeout,omitempty"`
|
|
ConnectingTimeout int `json:"connectingTimeout,omitempty" xml:"connectingTimeout,omitempty"`
|
|
HasPlaylist bool `json:"hasPlaylist" xml:"hasPlaylist"`
|
|
IsRealtime bool `json:"isRealtime" xml:"isRealtime"`
|
|
StreamUrl string `json:"streamUrl" xml:"streamUrl"`
|
|
}
|
|
|
|
// Audio represents audio content metadata including format and quality information.
|
|
type Audio struct {
|
|
HasPlaylist bool `json:"hasPlaylist" xml:"hasPlaylist"`
|
|
IsRealtime bool `json:"isRealtime" xml:"isRealtime"`
|
|
MaxTimeout int `json:"maxTimeout,omitempty" xml:"maxTimeout,omitempty"`
|
|
StreamUrl string `json:"streamUrl" xml:"streamUrl"`
|
|
Streams []Stream `json:"streams" xml:"streams>stream"`
|
|
}
|
|
|
|
// BmxPlaybackResponse represents a playback response from BMX services.
|
|
type BmxPlaybackResponse struct {
|
|
Links *Links `json:"_links,omitempty" xml:"links,omitempty"`
|
|
Artist struct {
|
|
Name string `json:"name,omitempty" xml:"name,omitempty"`
|
|
} `json:"artist,omitempty" xml:"artist,omitempty"`
|
|
Audio Audio `json:"audio" xml:"audio"`
|
|
ImageUrl string `json:"imageUrl" xml:"imageUrl"`
|
|
IsFavorite *bool `json:"isFavorite,omitempty" xml:"isFavorite,omitempty"`
|
|
Name string `json:"name" xml:"name"`
|
|
StreamType string `json:"streamType" xml:"streamType"`
|
|
Duration int `json:"duration,omitempty" xml:"duration,omitempty"`
|
|
ShuffleDisabled bool `json:"shuffle_disabled,omitempty" xml:"shuffleDisabled,omitempty"`
|
|
RepeatDisabled bool `json:"repeat_disabled,omitempty" xml:"repeatDisabled,omitempty"`
|
|
}
|
|
|
|
// Track represents track information for media playback.
|
|
type Track struct {
|
|
Links *Links `json:"_links,omitempty" xml:"links,omitempty"`
|
|
IsSelected bool `json:"isSelected" xml:"isSelected"`
|
|
Name string `json:"name" xml:"name"`
|
|
}
|
|
|
|
// BmxPodcastInfoResponse represents podcast information from BMX services.
|
|
type BmxPodcastInfoResponse struct {
|
|
Links *Links `json:"_links,omitempty" xml:"links,omitempty"`
|
|
Name string `json:"name" xml:"name"`
|
|
ShuffleDisabled bool `json:"shuffleDisabled" xml:"shuffleDisabled"`
|
|
RepeatDisabled bool `json:"repeatDisabled" xml:"repeatDisabled"`
|
|
StreamType string `json:"streamType" xml:"streamType"`
|
|
Tracks []Track `json:"tracks" xml:"tracks>track"`
|
|
}
|
|
|
|
// SourceProvider represents a media source provider configuration.
|
|
type SourceProvider struct {
|
|
ID int `json:"id" xml:"id,attr"`
|
|
CreatedOn string `json:"created_on" xml:"createdOn"`
|
|
Name string `json:"name" xml:"name"`
|
|
UpdatedOn string `json:"updated_on" xml:"updatedOn"`
|
|
}
|
|
|
|
// ServiceContentItem represents a media content item with source and location details.
|
|
type ServiceContentItem struct {
|
|
ID string `json:"id" xml:"id,attr"`
|
|
Name string `json:"name" xml:"name"`
|
|
Source string `json:"source,omitempty" xml:"source,attr,omitempty"`
|
|
Type string `json:"type" xml:"type,attr"`
|
|
ContentItemType string `json:"content_item_type" xml:"contentItemType"`
|
|
Location string `json:"location" xml:"location"`
|
|
SourceAccount string `json:"source_account,omitempty" xml:"sourceAccount,attr,omitempty"`
|
|
SourceID string `json:"source_id,omitempty" xml:"sourceid,omitempty"`
|
|
IsPresetable string `json:"is_presetable,omitempty" xml:"isPresetable,attr,omitempty"`
|
|
}
|
|
|
|
// ServicePreset represents a user-defined preset for quick access to media content.
|
|
type ServicePreset struct {
|
|
ServiceContentItem
|
|
ContainerArt string `json:"container_art" xml:"containerArt"`
|
|
CreatedOn string `json:"created_on" xml:"createdOn"`
|
|
UpdatedOn string `json:"updated_on" xml:"updatedOn"`
|
|
ButtonNumber string `json:"button_number,omitempty" xml:"buttonNumber,attr,omitempty"`
|
|
Username string `json:"-" xml:"username,omitempty"`
|
|
SourceConfig *ConfiguredSource `json:"-" xml:"source,omitempty"`
|
|
}
|
|
|
|
// ServiceRecent represents recently played media content.
|
|
type ServiceRecent struct {
|
|
XMLName xml.Name `json:"-" xml:"recent"`
|
|
ServiceContentItem
|
|
DeviceID string `json:"device_id" xml:"deviceid,attr"`
|
|
UtcTime string `json:"utc_time" xml:"utcTime,attr"`
|
|
CreatedOn string `json:"created_on,omitempty" xml:"createdOn"`
|
|
UpdatedOn string `json:"updated_on,omitempty" xml:"updatedOn"`
|
|
ContainerArt string `json:"container_art,omitempty" xml:"containerArt,omitempty"`
|
|
SourceConfig *ConfiguredSource `json:"-" xml:"source,omitempty"`
|
|
LastPlayedAt string `json:"last_played_at,omitempty" xml:"lastplayedat"`
|
|
}
|
|
|
|
// ConfiguredSource represents a configured media source with authentication details.
|
|
type ConfiguredSource struct {
|
|
XMLName xml.Name `json:"-" xml:"source"`
|
|
DisplayName string `json:"display_name" xml:"name"`
|
|
ID string `json:"id" xml:"id,attr"`
|
|
Secret string `json:"secret" xml:"credential"`
|
|
SecretType string `json:"secret_type" xml:"credential_type,attr"`
|
|
SourceKey struct {
|
|
Type string `xml:"type,attr"`
|
|
Account string `xml:"account,attr"`
|
|
} `json:"source_key" xml:"source_key"`
|
|
Type string `xml:"type,attr"`
|
|
|
|
// Parity fields
|
|
CreatedOn string `json:"created_on,omitempty" xml:"createdOn"`
|
|
UpdatedOn string `json:"updated_on,omitempty" xml:"updatedOn"`
|
|
SourceProviderID string `json:"sourceproviderid,omitempty" xml:"sourceproviderid"`
|
|
Username string `json:"username,omitempty" xml:"username"`
|
|
SourceName string `json:"source_name,omitempty" xml:"sourcename"`
|
|
SourceSettings string `json:"-" xml:"sourceSettings"`
|
|
|
|
// Legacy fields for backward compatibility in code if needed,
|
|
// though it's better to update the code to use SourceKey.
|
|
SourceKeyType string `json:"source_key_type" xml:"-"`
|
|
SourceKeyAccount string `json:"source_key_account" xml:"-"`
|
|
}
|
|
|
|
// MarshalXML implements the xml.Marshaler interface for custom XML encoding of ConfiguredSource.
|
|
func (s ConfiguredSource) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
type Alias ConfiguredSource
|
|
|
|
a := struct {
|
|
Alias
|
|
Username string `xml:"username"`
|
|
SourceName string `xml:"sourcename"`
|
|
SourceSettings string `xml:"sourceSettings"`
|
|
}{
|
|
Alias: Alias(s),
|
|
}
|
|
a.Username = s.Username
|
|
a.SourceName = s.SourceName
|
|
// We want <sourceSettings/>
|
|
a.SourceSettings = ""
|
|
|
|
return e.EncodeElement(a, start)
|
|
}
|
|
|
|
// ServiceDeviceInfo represents information about a SoundTouch device.
|
|
type ServiceDeviceInfo struct {
|
|
DeviceID string `json:"device_id" xml:"deviceID,attr"`
|
|
ProductCode string `json:"product_code" xml:"type"`
|
|
DeviceSerialNumber string `json:"device_serial_number" xml:"serialnumber"`
|
|
ProductSerialNumber string `json:"product_serial_number" xml:"product_serial_number"`
|
|
FirmwareVersion string `json:"firmware_version" xml:"softwareVersion"`
|
|
IPAddress string `json:"ip_address" xml:"ipAddress"`
|
|
Name string `json:"name" xml:"name"`
|
|
MacAddress string `json:"mac_address,omitempty" xml:"-"`
|
|
DiscoveryMethod string `json:"discovery_method,omitempty"`
|
|
AccountID string `json:"account_id,omitempty"`
|
|
Components []ServiceComponent `json:"components,omitempty" xml:"-"`
|
|
}
|
|
|
|
// ServiceComponent represents a hardware or software component of a device.
|
|
type ServiceComponent struct {
|
|
Type string `xml:"type,attr"`
|
|
Category string `xml:"category,attr,omitempty"`
|
|
SoftwareVersion string `xml:"firmware-version"`
|
|
SerialNumber string `xml:"serialnumber"`
|
|
Label string `xml:"componentlabel,omitempty"`
|
|
}
|
|
|
|
// CustomerSupportDevice represents device information for customer support purposes.
|
|
type CustomerSupportDevice struct {
|
|
ID string `xml:"id,attr"`
|
|
SerialNumber string `xml:"serialnumber"`
|
|
FirmwareVersion string `xml:"firmware-version"`
|
|
Product struct {
|
|
ProductCode string `xml:"product_code,attr"`
|
|
Type string `xml:"type,attr"`
|
|
SerialNumber string `xml:"serialnumber"`
|
|
} `xml:"product"`
|
|
}
|
|
|
|
// CustomerSupportRequest represents a customer support request with device and configuration details.
|
|
type CustomerSupportRequest struct {
|
|
XMLName xml.Name `xml:"device-data"`
|
|
Device CustomerSupportDevice `xml:"device"`
|
|
DiagnosticData struct {
|
|
DeviceLandscape struct {
|
|
RSSI string `xml:"rssi"`
|
|
GatewayIP string `xml:"gateway-ip-address"`
|
|
IPAddress string `xml:"ip-address"`
|
|
NetworkConnectionType string `xml:"network-connection-type"`
|
|
MacAddresses []string `xml:"macaddresses>macaddress"`
|
|
} `xml:"device-landscape"`
|
|
} `xml:"diagnostic-data"`
|
|
}
|
|
|
|
// UsageStats represents usage statistics for the service.
|
|
type UsageStats struct {
|
|
DeviceID string `json:"deviceId" xml:"deviceId"`
|
|
AccountID string `json:"accountId" xml:"accountId"`
|
|
Timestamp string `json:"timestamp" xml:"timestamp"`
|
|
EventType string `json:"eventType" xml:"eventType"`
|
|
Parameters map[string]interface{} `json:"parameters" xml:"parameters"`
|
|
}
|
|
|
|
// ErrorStats represents error statistics for monitoring and debugging.
|
|
type ErrorStats struct {
|
|
DeviceID string `json:"deviceId" xml:"deviceId"`
|
|
ErrorCode string `json:"errorCode" xml:"errorCode"`
|
|
ErrorMessage string `json:"errorMessage" xml:"errorMessage"`
|
|
Timestamp string `json:"timestamp" xml:"timestamp"`
|
|
Details string `json:"details,omitempty" xml:"details,omitempty"`
|
|
}
|
|
|
|
// DeviceEvent represents an event that occurred on a device.
|
|
type DeviceEvent struct {
|
|
Type string `json:"type"`
|
|
Time string `json:"time"`
|
|
MonoTime int64 `json:"monoTime"`
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
|
|
// DeviceEventsRequest represents a request containing multiple device events (stapp/scmudc).
|
|
type DeviceEventsRequest struct {
|
|
Envelope struct {
|
|
MonoTime int64 `json:"monoTime"`
|
|
PayloadProtocolVersion string `json:"payloadProtocolVersion"`
|
|
PayloadType string `json:"payloadType"`
|
|
ProtocolVersion string `json:"protocolVersion"`
|
|
Time string `json:"time"`
|
|
UniqueID string `json:"uniqueId"`
|
|
} `json:"envelope"`
|
|
Payload struct {
|
|
DeviceInfo struct {
|
|
BoseID string `json:"boseID"`
|
|
DeviceID string `json:"deviceID"`
|
|
DeviceType string `json:"deviceType"`
|
|
SoftwareVersion string `json:"softwareVersion"`
|
|
} `json:"deviceInfo"`
|
|
Events []struct {
|
|
Data map[string]interface{} `json:"data"`
|
|
Time string `json:"time"`
|
|
Type string `json:"type"`
|
|
} `json:"events"`
|
|
} `json:"payload"`
|
|
}
|
|
|
|
// DeviceSettingsResponse represents device settings.
|
|
type DeviceSettingsResponse struct {
|
|
XMLName xml.Name `xml:"deviceSettings"`
|
|
Settings []DeviceSetting `xml:"deviceSetting"`
|
|
}
|
|
|
|
// DeviceSetting represents a single device setting.
|
|
type DeviceSetting struct {
|
|
Name string `xml:"name"`
|
|
Value string `xml:"value"`
|
|
}
|
|
|
|
// AccountProfileResponse represents a customer account profile.
|
|
type AccountProfileResponse struct {
|
|
XMLName xml.Name `xml:"customer"`
|
|
AccountID string `xml:"accountID"`
|
|
Email string `xml:"email"`
|
|
FirstName string `xml:"firstName"`
|
|
LastName string `xml:"lastName"`
|
|
CountryCode string `xml:"countryCode"`
|
|
LanguageCode string `xml:"languageCode"`
|
|
Street string `xml:"street"`
|
|
City string `xml:"city"`
|
|
PostalCode string `xml:"postalCode"`
|
|
State string `xml:"state"`
|
|
Phone string `xml:"phone"`
|
|
MarketingOptIn bool `xml:"marketingOptIn"`
|
|
}
|
|
|
|
// ChangePasswordRequest represents a request to change the account password.
|
|
type ChangePasswordRequest struct {
|
|
XMLName xml.Name `xml:"passwordChange"`
|
|
OldPassword string `xml:"oldPassword"`
|
|
NewPassword string `xml:"newPassword"`
|
|
}
|
|
|
|
// EmailAddressResponse represents the account email address.
|
|
type EmailAddressResponse struct {
|
|
XMLName xml.Name `xml:"emailAddress"`
|
|
Email string `xml:",chardata"`
|
|
}
|
|
|
|
// FullResponseSource represents a configured media source specifically for the /full response.
|
|
// It follows the specific XML structure and field order of the upstream /full response.
|
|
type FullResponseSource struct {
|
|
ID string `xml:"id,attr"`
|
|
Type string `xml:"type,attr"`
|
|
CreatedOn string `xml:"createdOn"`
|
|
Credential struct {
|
|
Type string `xml:"type,attr"`
|
|
Value string `xml:",chardata"`
|
|
} `xml:"credential"`
|
|
Name string `xml:"name"`
|
|
SourceProviderID string `xml:"sourceproviderid"`
|
|
SourceName string `xml:"sourcename"`
|
|
SourceSettings string `xml:"sourceSettings"`
|
|
UpdatedOn string `xml:"updatedOn"`
|
|
Username string `xml:"username"`
|
|
}
|
|
|
|
// FullResponsePreset represents a preset specifically for the /full response.
|
|
type FullResponsePreset struct {
|
|
ButtonNumber string `xml:"buttonNumber,attr"`
|
|
ContainerArt string `xml:"containerArt"`
|
|
ContentItemType string `xml:"contentItemType"`
|
|
CreatedOn string `xml:"createdOn"`
|
|
Location string `xml:"location"`
|
|
Name string `xml:"name"`
|
|
Source FullResponseSource `xml:"source"`
|
|
UpdatedOn string `xml:"updatedOn"`
|
|
Username string `xml:"username"`
|
|
}
|
|
|
|
// FullResponseRecent represents a recent item specifically for the /full response.
|
|
type FullResponseRecent struct {
|
|
ID string `xml:"id,attr"`
|
|
ContentItemType string `xml:"contentItemType"`
|
|
CreatedOn string `xml:"createdOn"`
|
|
LastPlayedAt string `xml:"lastplayedat"`
|
|
Location string `xml:"location"`
|
|
Name string `xml:"name"`
|
|
Source FullResponseSource `xml:"source"`
|
|
SourceID string `xml:"sourceid"`
|
|
UpdatedOn string `xml:"updatedOn"`
|
|
}
|
|
|
|
// AccountFullResponse represents the complete account XML structure.
|
|
type AccountFullResponse struct {
|
|
XMLName xml.Name `xml:"account"`
|
|
ID string `xml:"id,attr"`
|
|
AccountStatus string `xml:"accountStatus"`
|
|
Devices []AccountDevice `xml:"devices>device"`
|
|
Mode string `xml:"mode"`
|
|
PreferredLanguage string `xml:"preferredLanguage"`
|
|
ProviderSettings []ProviderSetting `xml:"providerSettings>providerSetting"`
|
|
Sources []FullResponseSource `xml:"sources>source"`
|
|
}
|
|
|
|
// AccountDevice represents a device in the account response.
|
|
type AccountDevice struct {
|
|
DeviceID string `xml:"deviceid,attr"`
|
|
AttachedProduct *AttachedProduct `xml:"attachedProduct"`
|
|
CreatedOn string `xml:"createdOn"`
|
|
FirmwareVersion string `xml:"firmwareVersion"`
|
|
IPAddress string `xml:"ipaddress"`
|
|
Name string `xml:"name"`
|
|
Presets []FullResponsePreset `xml:"presets>preset"`
|
|
Recents []FullResponseRecent `xml:"recents>recent"`
|
|
SerialNumber string `xml:"serialNumber"`
|
|
UpdatedOn string `xml:"updatedOn"`
|
|
}
|
|
|
|
// AttachedProduct represents product information for a device.
|
|
type AttachedProduct struct {
|
|
ProductCode string `xml:"product_code,attr"`
|
|
Components []ServiceComponent `xml:"components>component"`
|
|
ProductLabel string `xml:"productlabel"`
|
|
SerialNumber string `xml:"serialNumber"`
|
|
UpdatedOn string `xml:"updatedOn"`
|
|
}
|
|
|
|
// ProviderSetting represents a single provider setting.
|
|
type ProviderSetting struct {
|
|
BoseID string `xml:"boseId"`
|
|
KeyName string `xml:"keyName"`
|
|
Value string `xml:"value"`
|
|
ProviderID string `xml:"providerId"`
|
|
}
|