diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go
index 1c15828..7ecd1a5 100644
--- a/cmd/soundtouch-service/main.go
+++ b/cmd/soundtouch-service/main.go
@@ -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 (
diff --git a/examples/service-demo/main.go b/examples/service-demo/main.go
index 0bbc117..5466b18 100644
--- a/examples/service-demo/main.go
+++ b/examples/service-demo/main.go
@@ -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 (
diff --git a/pkg/models/models.go b/pkg/models/models.go
index d9f2fba..c7a7a89 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -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"`
diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go
index 616dfe5..bdb077c 100644
--- a/pkg/service/datastore/datastore.go
+++ b/pkg/service/datastore/datastore.go
@@ -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()
diff --git a/pkg/service/marge/marge.go b/pkg/service/marge/marge.go
index 4a2d70d..dde7a09 100644
--- a/pkg/service/marge/marge.go
+++ b/pkg/service/marge/marge.go
@@ -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(`%s`, 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(`%s`, DateStr)
res += ``
}
@@ -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(`%s`, 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(`%s`, DateStr)
res += ``
}
@@ -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(`%sELIGIBLE_FOR_TRIALtrue14`, account)
}
+// SoftwareUpdateToXML generates software update configuration XML.
func SoftwareUpdateToXML() string {
return ``
}
+// 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)
}
diff --git a/pkg/service/setup/setup.go b/pkg/service/setup/setup.go
index ffd5674..f32f750 100644
--- a/pkg/service/setup/setup.go
+++ b/pkg/service/setup/setup.go
@@ -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 = "\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 = "\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)