diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go index ba87ed5..d6da4dc 100644 --- a/pkg/service/datastore/datastore.go +++ b/pkg/service/datastore/datastore.go @@ -1647,6 +1647,24 @@ func (ds *DataStore) RemoveDeviceDir(account, device string) error { return ds.RemoveDevice(account, device) } +// MoveDevice atomically moves a device directory from one account to another +// on the same filesystem. If the target account directory doesn't exist it is +// created. Returns an error if the rename fails (leaving the source intact). +func (ds *DataStore) MoveDevice(oldAccount, newAccount, deviceID string) error { + ds.fileMutex.Lock() + defer ds.fileMutex.Unlock() + + oldDir := ds.AccountDeviceDir(oldAccount, deviceID) + newDir := ds.AccountDeviceDir(newAccount, deviceID) + + newAccountDir := filepath.Dir(newDir) + if err := ds.rootMkdirAll(newAccountDir, 0755); err != nil { + return err + } + + return ds.rootRename(oldDir, newDir) +} + // DeduceSourceIDs updates the source IDs in the given slice by deducing them from recents and presets. func (ds *DataStore) DeduceSourceIDs(account, device string, sources []models.ConfiguredSource) { // Deduce source IDs from recents and presets diff --git a/pkg/service/datastore/datastore_test.go b/pkg/service/datastore/datastore_test.go index 0b0d7cf..acfce68 100644 --- a/pkg/service/datastore/datastore_test.go +++ b/pkg/service/datastore/datastore_test.go @@ -457,3 +457,40 @@ func TestSettingsPersistence(t *testing.T) { t.Errorf("Expected DiscoveryEnabled %v, got %v", settings.DiscoveryEnabled, loaded.DiscoveryEnabled) } } + +func TestMoveDeviceMigratesData(t *testing.T) { + tempDir := t.TempDir() + ds := NewDataStore(tempDir) + + oldAccount := "default" + newAccount := "8637922" + deviceID := "F4E11E930BEB" + + // Seed the device under the old account with presets + presets := []models.ServicePreset{ + {ID: "1", ServiceContentItem: models.ServiceContentItem{Name: "Radio Preset", ContentItemType: "stationurl"}}, + } + if err := ds.SavePresets(oldAccount, deviceID, presets); err != nil { + t.Fatalf("SavePresets under %s: %v", oldAccount, err) + } + + // Move the device to the new account + if err := ds.MoveDevice(oldAccount, newAccount, deviceID); err != nil { + t.Fatalf("MoveDevice %s → %s: %v", oldAccount, newAccount, err) + } + + // Verify presets survived the move under the new account + moved, err := ds.GetPresets(newAccount, deviceID) + if err != nil { + t.Fatalf("GetPresets under %s after move: %v", newAccount, err) + } + if len(moved) != 1 || moved[0].Name != "Radio Preset" { + t.Errorf("presets not preserved: got %+v", moved) + } + + // Verify the old account no longer has the device + oldPresets, err := ds.GetPresets(oldAccount, deviceID) + if err == nil && len(oldPresets) > 0 { + t.Errorf("old account %s still has presets after move: %+v", oldAccount, oldPresets) + } +} diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index 9600703..53ad5ab 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -1058,19 +1058,12 @@ func (s *Server) handleDiscoveredDevice(d models.DiscoveredDevice) { } // If the speaker reports a paired account that differs from the stored - // location, clean up the stale entry so ListAllDevices doesn't return duplicates. + // location, migrate the device directory so ListAllDevices doesn't return duplicates. if liveInfo.MargeAccountUUID != "" && storedAccount != "" && liveInfo.MargeAccountUUID != storedAccount { - // Preserve presets, recents and sources before cleaning up - if presets, err := s.ds.GetPresets(storedAccount, deviceID); err == nil && len(presets) > 0 { - _ = s.ds.SavePresets(liveInfo.MargeAccountUUID, deviceID, presets) + if err := s.ds.MoveDevice(storedAccount, accountID, deviceID); err != nil { + log.Printf("Failed to migrate device %s from %s to %s: %v", + deviceID, storedAccount, accountID, err) } - if recents, err := s.ds.GetRecents(storedAccount, deviceID); err == nil && len(recents) > 0 { - _ = s.ds.SaveRecents(liveInfo.MargeAccountUUID, deviceID, recents) - } - if sources, err := s.ds.GetConfiguredSources(storedAccount, deviceID); err == nil && len(sources) > 0 { - _ = s.ds.SaveConfiguredSources(liveInfo.MargeAccountUUID, deviceID, sources) - } - s.ds.RemoveDevice(storedAccount, deviceID) } // 4. Get primary MAC address from networkInfo