mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
chore: apply final linter fixes and code quality improvements across the service layer
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Package main provides the SoundTouch service daemon that acts as a proxy and management
|
||||
// interface for Bose SoundTouch devices, providing Marge service emulation and device discovery.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package main provides a demo client for the SoundTouch service API,
|
||||
// demonstrating how to interact with devices and retrieve media information.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// 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"`
|
||||
@@ -22,6 +27,7 @@ type Links struct {
|
||||
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"`
|
||||
@@ -30,6 +36,7 @@ type IconSet struct {
|
||||
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"`
|
||||
@@ -38,11 +45,13 @@ type Asset struct {
|
||||
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"`
|
||||
@@ -54,12 +63,14 @@ type BmxService struct {
|
||||
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"`
|
||||
@@ -69,6 +80,7 @@ type Stream struct {
|
||||
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"`
|
||||
@@ -77,6 +89,7 @@ type Audio struct {
|
||||
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 {
|
||||
@@ -92,12 +105,14 @@ type BmxPlaybackResponse struct {
|
||||
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"`
|
||||
@@ -107,6 +122,7 @@ type BmxPodcastInfoResponse struct {
|
||||
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"`
|
||||
@@ -114,6 +130,7 @@ type SourceProvider struct {
|
||||
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:"itemName"`
|
||||
@@ -125,6 +142,7 @@ type ServiceContentItem struct {
|
||||
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"`
|
||||
@@ -132,6 +150,7 @@ type ServicePreset struct {
|
||||
UpdatedOn string `json:"updated_on" xml:"updatedOn"`
|
||||
}
|
||||
|
||||
// ServiceRecent represents recently played media content.
|
||||
type ServiceRecent struct {
|
||||
ServiceContentItem
|
||||
DeviceID string `json:"device_id" xml:"deviceid"`
|
||||
@@ -139,6 +158,7 @@ type ServiceRecent struct {
|
||||
ContainerArt string `json:"container_art,omitempty" xml:"containerArt,omitempty"`
|
||||
}
|
||||
|
||||
// ConfiguredSource represents a configured media source with authentication details.
|
||||
type ConfiguredSource struct {
|
||||
DisplayName string `json:"display_name" xml:"sourcename"`
|
||||
ID string `json:"id" xml:"id,attr"`
|
||||
@@ -148,6 +168,7 @@ type ConfiguredSource struct {
|
||||
SourceKeyAccount string `json:"source_key_account" xml:"username"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
@@ -158,6 +179,7 @@ type ServiceDeviceInfo struct {
|
||||
Name string `json:"name" xml:"name"`
|
||||
}
|
||||
|
||||
// CustomerSupportDevice represents device information for customer support purposes.
|
||||
type CustomerSupportDevice struct {
|
||||
ID string `xml:"id,attr"`
|
||||
SerialNumber string `xml:"serialnumber"`
|
||||
@@ -169,6 +191,7 @@ type CustomerSupportDevice struct {
|
||||
} `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"`
|
||||
@@ -183,6 +206,7 @@ type CustomerSupportRequest struct {
|
||||
} `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"`
|
||||
@@ -191,6 +215,7 @@ type UsageStats struct {
|
||||
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"`
|
||||
@@ -199,6 +224,7 @@ type ErrorStats struct {
|
||||
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"`
|
||||
|
||||
@@ -28,6 +28,7 @@ type DataStore struct {
|
||||
}
|
||||
|
||||
// NewDataStore creates a new DataStore.
|
||||
// NewDataStore creates a new DataStore instance with the specified data directory.
|
||||
func NewDataStore(dataDir string) *DataStore {
|
||||
if dataDir == "" {
|
||||
dataDir = "data"
|
||||
@@ -39,18 +40,22 @@ func NewDataStore(dataDir string) *DataStore {
|
||||
}
|
||||
}
|
||||
|
||||
// AccountDir returns the directory path for a specific account.
|
||||
func (ds *DataStore) AccountDir(account string) string {
|
||||
return filepath.Join(ds.DataDir, account)
|
||||
}
|
||||
|
||||
// AccountDevicesDir returns the devices directory path for a specific account.
|
||||
func (ds *DataStore) AccountDevicesDir(account string) string {
|
||||
return filepath.Join(ds.DataDir, account, constants.DevicesDir)
|
||||
}
|
||||
|
||||
// AccountDeviceDir returns the directory path for a specific device within an account.
|
||||
func (ds *DataStore) AccountDeviceDir(account, device string) string {
|
||||
return filepath.Join(ds.AccountDevicesDir(account), device)
|
||||
}
|
||||
|
||||
// GetDeviceInfo retrieves device information for the specified account and device.
|
||||
func (ds *DataStore) GetDeviceInfo(account, device string) (*models.ServiceDeviceInfo, error) {
|
||||
path := filepath.Join(ds.AccountDeviceDir(account, device), constants.DeviceInfoFile)
|
||||
|
||||
@@ -245,6 +250,7 @@ func (ds *DataStore) parseDeviceInfoFile(path string) (*models.ServiceDeviceInfo
|
||||
return deviceInfo, nil
|
||||
}
|
||||
|
||||
// GetPresets retrieves all presets for the specified account.
|
||||
func (ds *DataStore) GetPresets(account string) ([]models.ServicePreset, error) {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.PresetsFile)
|
||||
|
||||
@@ -297,6 +303,7 @@ func (ds *DataStore) GetPresets(account string) ([]models.ServicePreset, error)
|
||||
return presets, nil
|
||||
}
|
||||
|
||||
// SavePresets saves the preset list for the specified account.
|
||||
func (ds *DataStore) SavePresets(account string, presets []models.ServicePreset) error {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.PresetsFile)
|
||||
|
||||
@@ -350,6 +357,7 @@ func (ds *DataStore) SavePresets(account string, presets []models.ServicePreset)
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
// GetRecents retrieves all recent items for the specified account.
|
||||
func (ds *DataStore) GetRecents(account string) ([]models.ServiceRecent, error) {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.RecentsFile)
|
||||
|
||||
@@ -402,6 +410,7 @@ func (ds *DataStore) GetRecents(account string) ([]models.ServiceRecent, error)
|
||||
return recents, nil
|
||||
}
|
||||
|
||||
// SaveRecents saves the recent items list for the specified account.
|
||||
func (ds *DataStore) SaveRecents(account string, recents []models.ServiceRecent) error {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.RecentsFile)
|
||||
|
||||
@@ -460,6 +469,7 @@ func (ds *DataStore) SaveRecents(account string, recents []models.ServiceRecent)
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
// SaveDeviceInfo saves device information for the specified account and device.
|
||||
func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.ServiceDeviceInfo) error {
|
||||
if device == "" {
|
||||
return fmt.Errorf("device ID/name cannot be empty")
|
||||
@@ -541,11 +551,13 @@ func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.Service
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
// RemoveDevice removes a device and all its data from the specified account.
|
||||
func (ds *DataStore) RemoveDevice(account, device string) error {
|
||||
dir := ds.AccountDeviceDir(account, device)
|
||||
return os.RemoveAll(dir)
|
||||
}
|
||||
|
||||
// GetConfiguredSources retrieves all configured sources for the specified account.
|
||||
func (ds *DataStore) GetConfiguredSources(account string) ([]models.ConfiguredSource, error) {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.SourcesFile)
|
||||
|
||||
@@ -595,6 +607,7 @@ func (ds *DataStore) GetConfiguredSources(account string) ([]models.ConfiguredSo
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// SaveConfiguredSources saves the configured sources list for the specified account.
|
||||
func (ds *DataStore) SaveConfiguredSources(account string, sources []models.ConfiguredSource) error {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.SourcesFile)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
@@ -641,6 +654,7 @@ func (ds *DataStore) SaveConfiguredSources(account string, sources []models.Conf
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
// Initialize creates the necessary directory structure for the datastore.
|
||||
func (ds *DataStore) Initialize() error {
|
||||
// Ensure base data directory exists
|
||||
if err := os.MkdirAll(ds.DataDir, 0755); err != nil {
|
||||
@@ -661,6 +675,7 @@ func (ds *DataStore) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetETagForPresets returns the ETag (modification time) for the presets file.
|
||||
func (ds *DataStore) GetETagForPresets(account string) int64 {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.PresetsFile)
|
||||
|
||||
@@ -672,6 +687,7 @@ func (ds *DataStore) GetETagForPresets(account string) int64 {
|
||||
return info.ModTime().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
// GetETagForSources returns the ETag (modification time) for the sources file.
|
||||
func (ds *DataStore) GetETagForSources(account string) int64 {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.SourcesFile)
|
||||
|
||||
@@ -683,6 +699,7 @@ func (ds *DataStore) GetETagForSources(account string) int64 {
|
||||
return info.ModTime().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
// GetETagForRecents returns the ETag (modification time) for the recents file.
|
||||
func (ds *DataStore) GetETagForRecents(account string) int64 {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.RecentsFile)
|
||||
|
||||
@@ -694,6 +711,7 @@ func (ds *DataStore) GetETagForRecents(account string) int64 {
|
||||
return info.ModTime().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
// GetETagForAccount returns the highest ETag among presets, sources, and recents for the account.
|
||||
func (ds *DataStore) GetETagForAccount(account string) int64 {
|
||||
e1 := ds.GetETagForPresets(account)
|
||||
e2 := ds.GetETagForSources(account)
|
||||
@@ -711,6 +729,7 @@ func (ds *DataStore) GetETagForAccount(account string) int64 {
|
||||
return maxETag
|
||||
}
|
||||
|
||||
// SaveUsageStats saves usage statistics to the datastore.
|
||||
func (ds *DataStore) SaveUsageStats(stats models.UsageStats) error {
|
||||
dir := filepath.Join(ds.DataDir, "stats", "usage")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
@@ -728,6 +747,7 @@ func (ds *DataStore) SaveUsageStats(stats models.UsageStats) error {
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
// SaveErrorStats saves error statistics to the datastore.
|
||||
func (ds *DataStore) SaveErrorStats(stats models.ErrorStats) error {
|
||||
dir := filepath.Join(ds.DataDir, "stats", "error")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
@@ -745,6 +765,7 @@ func (ds *DataStore) SaveErrorStats(stats models.ErrorStats) error {
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
// AddDeviceEvent adds a device event to the in-memory event store.
|
||||
func (ds *DataStore) AddDeviceEvent(deviceID string, event models.DeviceEvent) {
|
||||
ds.eventMutex.Lock()
|
||||
defer ds.eventMutex.Unlock()
|
||||
@@ -760,6 +781,7 @@ func (ds *DataStore) AddDeviceEvent(deviceID string, event models.DeviceEvent) {
|
||||
ds.deviceEvents[deviceID] = events
|
||||
}
|
||||
|
||||
// GetDeviceEvents retrieves all events for the specified device.
|
||||
func (ds *DataStore) GetDeviceEvents(deviceID string) []models.DeviceEvent {
|
||||
ds.eventMutex.RLock()
|
||||
defer ds.eventMutex.RUnlock()
|
||||
|
||||
+18
-17
@@ -1,3 +1,5 @@
|
||||
// Package marge provides XML generation and data management for the Marge service,
|
||||
// which handles SoundTouch device configuration, presets, recents, and account management.
|
||||
package marge
|
||||
|
||||
import (
|
||||
@@ -12,8 +14,10 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
)
|
||||
|
||||
// DateStr is a fixed timestamp used in XML responses for consistency.
|
||||
const DateStr = "2012-09-19T12:43:00.000+00:00"
|
||||
|
||||
// SourceProviders returns a list of available media source providers.
|
||||
func SourceProviders() []models.SourceProvider {
|
||||
providers := make([]models.SourceProvider, len(constants.Providers))
|
||||
for i, name := range constants.Providers {
|
||||
@@ -28,11 +32,13 @@ func SourceProviders() []models.SourceProvider {
|
||||
return providers
|
||||
}
|
||||
|
||||
// SourceProvidersXML represents the XML structure for source providers.
|
||||
type SourceProvidersXML struct {
|
||||
XMLName xml.Name `xml:"sourceProviders"`
|
||||
Providers []models.SourceProvider `xml:"sourceProvider"`
|
||||
}
|
||||
|
||||
// SourceProvidersToXML converts source providers to XML format.
|
||||
func SourceProvidersToXML() ([]byte, error) {
|
||||
sp := SourceProvidersXML{
|
||||
Providers: SourceProviders(),
|
||||
@@ -46,6 +52,7 @@ func SourceProvidersToXML() ([]byte, error) {
|
||||
return append([]byte(xml.Header), data...), nil
|
||||
}
|
||||
|
||||
// ConfiguredSourceToXML converts a configured source to XML format.
|
||||
func ConfiguredSourceToXML(cs models.ConfiguredSource) ([]byte, error) {
|
||||
type SourceXML struct {
|
||||
XMLName xml.Name `xml:"source"`
|
||||
@@ -89,6 +96,7 @@ func ConfiguredSourceToXML(cs models.ConfiguredSource) ([]byte, error) {
|
||||
return xml.Marshal(sxml)
|
||||
}
|
||||
|
||||
// GetConfiguredSourceXML returns the XML representation of a configured source as a string.
|
||||
func GetConfiguredSourceXML(cs models.ConfiguredSource) string {
|
||||
providerID := 0
|
||||
|
||||
@@ -103,6 +111,7 @@ func GetConfiguredSourceXML(cs models.ConfiguredSource) string {
|
||||
cs.ID, DateStr, cs.Secret, cs.SourceKeyAccount, providerID, cs.DisplayName, DateStr, cs.SourceKeyAccount)
|
||||
}
|
||||
|
||||
// PresetsToXML converts account presets to XML format for Marge responses.
|
||||
func PresetsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
presets, err := ds.GetPresets(account)
|
||||
if err != nil {
|
||||
@@ -126,21 +135,13 @@ func PresetsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
res += fmt.Sprintf(`<name>%s</name>`, p.Name)
|
||||
|
||||
// Content Item Source
|
||||
found := false
|
||||
|
||||
for _, s := range sources {
|
||||
if s.ID == p.SourceID || (s.SourceKeyType == p.Source && s.SourceKeyAccount == p.SourceAccount) {
|
||||
res += GetConfiguredSourceXML(s)
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// This might happen if source is not found
|
||||
}
|
||||
|
||||
res += fmt.Sprintf(`<updatedOn>%s</updatedOn>`, DateStr)
|
||||
res += `</preset>`
|
||||
}
|
||||
@@ -150,6 +151,7 @@ func PresetsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
return append([]byte(xml.Header), []byte(res)...), nil
|
||||
}
|
||||
|
||||
// RecentsToXML converts account recent items to XML format for Marge responses.
|
||||
func RecentsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
recents, err := ds.GetRecents(account)
|
||||
if err != nil {
|
||||
@@ -179,21 +181,13 @@ func RecentsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
res += fmt.Sprintf(`<name>%s</name>`, r.Name)
|
||||
|
||||
// Content Item Source
|
||||
found := false
|
||||
|
||||
for _, s := range sources {
|
||||
if s.ID == r.SourceID || (s.SourceKeyType == r.Source && s.SourceKeyAccount == r.SourceAccount) {
|
||||
res += GetConfiguredSourceXML(s)
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// This might happen if source is not found
|
||||
}
|
||||
|
||||
res += fmt.Sprintf(`<updatedOn>%s</updatedOn>`, DateStr)
|
||||
res += `</recent>`
|
||||
}
|
||||
@@ -203,14 +197,17 @@ func RecentsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
return append([]byte(xml.Header), []byte(res)...), nil
|
||||
}
|
||||
|
||||
// ProviderSettingsToXML generates provider settings XML for the specified account.
|
||||
func ProviderSettingsToXML(account string) string {
|
||||
return fmt.Sprintf(`<providerSettings><providerSetting><boseId>%s</boseId><keyName>ELIGIBLE_FOR_TRIAL</keyName><value>true</value><providerId>14</providerId></providerSetting></providerSettings>`, account)
|
||||
}
|
||||
|
||||
// SoftwareUpdateToXML generates software update configuration XML.
|
||||
func SoftwareUpdateToXML() string {
|
||||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><software_update><softwareUpdateLocation></softwareUpdateLocation></software_update>`
|
||||
}
|
||||
|
||||
// AccountFullToXML generates a complete account XML with devices, presets, and recents.
|
||||
func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
devicesDir := ds.AccountDevicesDir(account)
|
||||
|
||||
@@ -275,7 +272,8 @@ func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
return []byte(res), nil
|
||||
}
|
||||
|
||||
func UpdatePreset(ds *datastore.DataStore, account, device string, presetNumber int, sourceXML []byte) ([]byte, error) {
|
||||
// UpdatePreset updates or creates a preset for the specified account and device.
|
||||
func UpdatePreset(ds *datastore.DataStore, account, _ string, presetNumber int, sourceXML []byte) ([]byte, error) {
|
||||
sources, err := ds.GetConfiguredSources(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -351,6 +349,7 @@ func UpdatePreset(ds *datastore.DataStore, account, device string, presetNumber
|
||||
return append([]byte(xml.Header), []byte(res)...), nil
|
||||
}
|
||||
|
||||
// AddRecent adds or updates a recent item for the specified account and device.
|
||||
func AddRecent(ds *datastore.DataStore, account, device string, sourceXML []byte) ([]byte, error) {
|
||||
sources, err := ds.GetConfiguredSources(account)
|
||||
if err != nil {
|
||||
@@ -476,6 +475,7 @@ func formatRecentResponse(recentObj *models.ServiceRecent, matchingSrc *models.C
|
||||
return append([]byte(xml.Header), []byte(res)...)
|
||||
}
|
||||
|
||||
// AddDeviceToAccount adds a new device to the specified account.
|
||||
func AddDeviceToAccount(ds *datastore.DataStore, account string, sourceXML []byte) ([]byte, error) {
|
||||
var newDeviceElem struct {
|
||||
DeviceID string `xml:"deviceid,attr"`
|
||||
@@ -506,6 +506,7 @@ func AddDeviceToAccount(ds *datastore.DataStore, account string, sourceXML []byt
|
||||
return append([]byte(xml.Header), []byte(res)...), nil
|
||||
}
|
||||
|
||||
// RemoveDeviceFromAccount removes a device from the specified account.
|
||||
func RemoveDeviceFromAccount(ds *datastore.DataStore, account, device string) error {
|
||||
return ds.RemoveDevice(account, device)
|
||||
}
|
||||
|
||||
+109
-114
@@ -4,7 +4,6 @@ package setup
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
@@ -119,7 +118,59 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
SSHSuccess: false,
|
||||
}
|
||||
|
||||
// 0. Populate from datastore if available
|
||||
// Populate device info from datastore and live info
|
||||
m.populateDeviceInfo(summary, deviceIP)
|
||||
|
||||
// 1. Initial planned config
|
||||
plannedCfg := PrivateCfg{
|
||||
MargeServerUrl: fmt.Sprintf("%s/marge", targetURL),
|
||||
StatsServerUrl: targetURL,
|
||||
SwUpdateUrl: fmt.Sprintf("%s/updates/soundtouch", targetURL),
|
||||
UsePandoraProductionServer: true,
|
||||
IsZeroconfEnabled: true,
|
||||
SaveMargeCustomerReport: false,
|
||||
BmxRegistryUrl: fmt.Sprintf("%s/bmx/registry/v1/services", targetURL),
|
||||
}
|
||||
|
||||
// 2. Check SSH and read current config
|
||||
currentConfig, err := m.checkCurrentConfig(summary, deviceIP)
|
||||
if err == nil && currentConfig != "" {
|
||||
summary.CurrentConfig = currentConfig
|
||||
fmt.Printf("Current config from %s (length: %d):\n%q\n", deviceIP, len(currentConfig), currentConfig)
|
||||
|
||||
// Parse current config
|
||||
var currentCfg PrivateCfg
|
||||
if xml.Unmarshal([]byte(currentConfig), ¤tCfg) == nil {
|
||||
summary.ParsedCurrentConfig = ¤tCfg
|
||||
|
||||
if proxyURL == "" {
|
||||
proxyURL = targetURL
|
||||
}
|
||||
|
||||
// Apply options if provided
|
||||
if options != nil {
|
||||
m.applyProxyOptions(&plannedCfg, proxyURL, options, ¤tCfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: CurrentConfig is set by checkCurrentConfig in all cases (success or failure)
|
||||
|
||||
xmlContent, err := xml.MarshalIndent(plannedCfg, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal planned XML: %w", err)
|
||||
}
|
||||
|
||||
summary.PlannedConfig = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + string(xmlContent)
|
||||
|
||||
// 3. Check for remote services files
|
||||
m.checkRemoteServices(summary, deviceIP)
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// populateDeviceInfo fills in device information from datastore and live info
|
||||
func (m *Manager) populateDeviceInfo(summary *MigrationSummary, deviceIP string) {
|
||||
// Populate from datastore if available
|
||||
if m.DataStore != nil {
|
||||
devices, err := m.DataStore.ListAllDevices()
|
||||
if err == nil {
|
||||
@@ -135,14 +186,11 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
|
||||
break
|
||||
}
|
||||
} else {
|
||||
log.Printf("Warning: failed to list devices from datastore: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 0a. Supplement with live info from :8090/info
|
||||
infoXML, err := m.GetLiveDeviceInfo(deviceIP)
|
||||
if err == nil {
|
||||
// Supplement with live info from :8090/info
|
||||
if infoXML, err := m.GetLiveDeviceInfo(deviceIP); err == nil {
|
||||
if infoXML.Name != "" {
|
||||
summary.DeviceName = infoXML.Name
|
||||
}
|
||||
@@ -158,119 +206,83 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
if infoXML.SoftwareVer != "" {
|
||||
summary.FirmwareVersion = infoXML.SoftwareVer
|
||||
}
|
||||
} else {
|
||||
log.Printf("Warning: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Initial planned config
|
||||
plannedCfg := PrivateCfg{
|
||||
MargeServerUrl: fmt.Sprintf("%s/marge", targetURL),
|
||||
StatsServerUrl: targetURL,
|
||||
SwUpdateUrl: fmt.Sprintf("%s/updates/soundtouch", targetURL),
|
||||
UsePandoraProductionServer: true,
|
||||
IsZeroconfEnabled: true,
|
||||
SaveMargeCustomerReport: false,
|
||||
BmxRegistryUrl: fmt.Sprintf("%s/bmx/registry/v1/services", targetURL),
|
||||
}
|
||||
|
||||
// 2. Check SSH and read current config
|
||||
var currentConfig string
|
||||
|
||||
// checkCurrentConfig reads and validates the current speaker configuration
|
||||
func (m *Manager) checkCurrentConfig(summary *MigrationSummary, deviceIP string) (string, error) {
|
||||
path := SoundTouchSdkPrivateCfgPath
|
||||
client := ssh.NewClient(deviceIP)
|
||||
|
||||
// Check if .original exists
|
||||
if _, checkErr := client.Run(fmt.Sprintf("[ -f %s.original ]", path)); checkErr == nil {
|
||||
originalConfig, _ := client.Run(fmt.Sprintf("cat %s.original", path))
|
||||
if originalConfig != "" {
|
||||
if originalConfig, _ := client.Run(fmt.Sprintf("cat %s.original", path)); originalConfig != "" {
|
||||
summary.OriginalConfig = originalConfig
|
||||
}
|
||||
}
|
||||
|
||||
// Check file details
|
||||
fileInfo, _ := client.Run(fmt.Sprintf("ls -l %s", path))
|
||||
if fileInfo != "" {
|
||||
fmt.Printf("File info for %s: %s\n", path, fileInfo)
|
||||
}
|
||||
|
||||
// Try cat
|
||||
// Try to read current config
|
||||
config, err := client.Run(fmt.Sprintf("cat %s", path))
|
||||
if err == nil && config != "" {
|
||||
currentConfig = config
|
||||
summary.SSHSuccess = true
|
||||
summary.CurrentConfig = currentConfig
|
||||
fmt.Printf("Current config from %s at %s (length: %d):\n%q\n", deviceIP, path, len(currentConfig), currentConfig)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Parse current config
|
||||
var currentCfg PrivateCfg
|
||||
if xml.Unmarshal([]byte(currentConfig), ¤tCfg) == nil {
|
||||
summary.ParsedCurrentConfig = ¤tCfg
|
||||
// Fallback: try base64 if cat returned empty string but file has size > 0
|
||||
if config == "" {
|
||||
if fileInfo, _ := client.Run(fmt.Sprintf("ls -l %s", path)); fileInfo != "" {
|
||||
if b64Config, configErr := client.Run(fmt.Sprintf("base64 %s", path)); configErr == nil && b64Config != "" {
|
||||
// File exists but couldn't read content properly
|
||||
summary.SSHSuccess = true
|
||||
summary.CurrentConfig = fmt.Sprintf("Error reading config: %v", err)
|
||||
|
||||
if proxyURL == "" {
|
||||
proxyURL = targetURL
|
||||
return "", fmt.Errorf("config file exists but couldn't read content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply options if provided
|
||||
if options != nil {
|
||||
// Marge
|
||||
if options["marge"] == "original" {
|
||||
plannedCfg.MargeServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.MargeServerUrl)
|
||||
}
|
||||
// Stats
|
||||
if options["stats"] == "original" {
|
||||
plannedCfg.StatsServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.StatsServerUrl)
|
||||
}
|
||||
// SwUpdate
|
||||
if options["sw_update"] == "original" {
|
||||
plannedCfg.SwUpdateUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.SwUpdateUrl)
|
||||
}
|
||||
// BMX
|
||||
if options["bmx"] == "original" {
|
||||
plannedCfg.BmxRegistryUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.BmxRegistryUrl)
|
||||
}
|
||||
} else if proxyURL != "" {
|
||||
// Default to proxy everything if proxyURL is explicitly provided but no options
|
||||
// (Maintain backward compatibility for now if needed, but we'll probably always pass options from UI)
|
||||
// Actually, if proxyURL is set but no options, let's keep the previous behavior of proxying all.
|
||||
plannedCfg.MargeServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.MargeServerUrl)
|
||||
plannedCfg.StatsServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.StatsServerUrl)
|
||||
plannedCfg.SwUpdateUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.SwUpdateUrl)
|
||||
plannedCfg.BmxRegistryUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.BmxRegistryUrl)
|
||||
}
|
||||
// If SSH failed or file couldn't be read, check if SSH connection works at all
|
||||
if _, sshErr := client.Run("ls /"); sshErr == nil {
|
||||
summary.SSHSuccess = true
|
||||
if err != nil {
|
||||
summary.CurrentConfig = fmt.Sprintf("Error reading config: %v", err)
|
||||
} else {
|
||||
summary.CurrentConfig = config // Might be empty
|
||||
}
|
||||
} else {
|
||||
// Fallback: try base64 if cat returned empty string but file has size > 0
|
||||
if config == "" && fileInfo != "" {
|
||||
fmt.Printf("Cat returned empty for %s, trying base64\n", path)
|
||||
|
||||
b64Config, configErr := client.Run(fmt.Sprintf("base64 %s", path))
|
||||
if configErr == nil && b64Config != "" {
|
||||
fmt.Printf("Base64 output for %s (length %d)\n", path, len(b64Config))
|
||||
}
|
||||
}
|
||||
|
||||
// If SSH failed or file couldn't be read
|
||||
if _, sshErr := client.Run("ls /"); sshErr == nil {
|
||||
summary.SSHSuccess = true
|
||||
if err != nil {
|
||||
summary.CurrentConfig = fmt.Sprintf("Error reading config: %v", err)
|
||||
} else {
|
||||
summary.CurrentConfig = config // Might be empty
|
||||
}
|
||||
} else {
|
||||
summary.SSHSuccess = false
|
||||
summary.CurrentConfig = fmt.Sprintf("SSH connection failed: %v", sshErr)
|
||||
}
|
||||
summary.SSHSuccess = false
|
||||
summary.CurrentConfig = fmt.Sprintf("SSH connection failed: %v", sshErr)
|
||||
}
|
||||
|
||||
xmlContent, err := xml.MarshalIndent(plannedCfg, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal planned XML: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// applyProxyOptions modifies planned config based on proxy options
|
||||
func (m *Manager) applyProxyOptions(plannedCfg *PrivateCfg, proxyURL string, options map[string]string, currentCfg *PrivateCfg) {
|
||||
if proxyURL == "" || currentCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
summary.PlannedConfig = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + string(xmlContent)
|
||||
if options["marge"] == "original" && currentCfg.MargeServerUrl != "" {
|
||||
plannedCfg.MargeServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.MargeServerUrl)
|
||||
}
|
||||
|
||||
// 3. Check for remote services files
|
||||
if options["stats"] == "original" && currentCfg.StatsServerUrl != "" {
|
||||
plannedCfg.StatsServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.StatsServerUrl)
|
||||
}
|
||||
|
||||
if options["sw_update"] == "original" && currentCfg.SwUpdateUrl != "" {
|
||||
plannedCfg.SwUpdateUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.SwUpdateUrl)
|
||||
}
|
||||
|
||||
if options["bmx"] == "original" && currentCfg.BmxRegistryUrl != "" {
|
||||
plannedCfg.BmxRegistryUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.BmxRegistryUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// checkRemoteServices checks for remote services files on the device
|
||||
func (m *Manager) checkRemoteServices(summary *MigrationSummary, deviceIP string) {
|
||||
client := ssh.NewClient(deviceIP)
|
||||
locations := []string{
|
||||
"/etc/remote_services",
|
||||
"/mnt/nv/remote_services",
|
||||
@@ -278,8 +290,7 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
}
|
||||
|
||||
for _, loc := range locations {
|
||||
_, err := client.Run(fmt.Sprintf("[ -e %s ]", loc))
|
||||
if err == nil {
|
||||
if _, err := client.Run(fmt.Sprintf("[ -e %s ]", loc)); err == nil {
|
||||
summary.RemoteServicesFound = append(summary.RemoteServicesFound, loc)
|
||||
|
||||
summary.RemoteServicesEnabled = true
|
||||
@@ -288,8 +299,6 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// MigrateSpeaker configures the speaker at the given IP to use this soundcork service.
|
||||
@@ -324,21 +333,7 @@ func (m *Manager) MigrateSpeaker(deviceIP, targetURL, proxyURL string, options m
|
||||
}
|
||||
|
||||
if options != nil {
|
||||
if options["marge"] == "original" {
|
||||
cfg.MargeServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.MargeServerUrl)
|
||||
}
|
||||
|
||||
if options["stats"] == "original" {
|
||||
cfg.StatsServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.StatsServerUrl)
|
||||
}
|
||||
|
||||
if options["sw_update"] == "original" {
|
||||
cfg.SwUpdateUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.SwUpdateUrl)
|
||||
}
|
||||
|
||||
if options["bmx"] == "original" {
|
||||
cfg.BmxRegistryUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.BmxRegistryUrl)
|
||||
}
|
||||
m.applyProxyOptions(&cfg, proxyURL, options, ¤tCfg)
|
||||
} else if proxyURL != "" {
|
||||
cfg.MargeServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.MargeServerUrl)
|
||||
cfg.StatsServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.StatsServerUrl)
|
||||
|
||||
Reference in New Issue
Block a user