mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Remove device-local Spotify primer
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -216,94 +215,6 @@ func (s *Server) HandleMgmtSpotifyAccounts(w http.ResponseWriter, _ *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMgmtInstallSpotifyPrimer installs the Spotify boot primer on a device identified by deviceId.
|
||||
// The deviceId can be either a known device ID/serial stored in the DataStore or a raw IP address.
|
||||
func (s *Server) HandleMgmtInstallSpotifyPrimer(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID := chi.URLParam(r, "deviceId")
|
||||
if deviceID == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device ID is required"}); err != nil {
|
||||
log.Printf("[Mgmt] Failed to encode error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
targetURL := r.URL.Query().Get("target_url")
|
||||
if targetURL == "" {
|
||||
s.mu.RLock()
|
||||
targetURL = s.sm.ServerURL
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Resolve deviceID to IP if possible
|
||||
deviceIP := ""
|
||||
|
||||
// If the ID looks like an IP, use it directly
|
||||
if strings.Count(deviceID, ".") == 3 || strings.Contains(deviceID, ":") { // IPv4 or IPv6
|
||||
deviceIP = deviceID
|
||||
} else {
|
||||
// Look up in datastore
|
||||
all, err := s.ds.ListAllDevices()
|
||||
if err == nil {
|
||||
for i := range all {
|
||||
d := &all[i]
|
||||
if d.DeviceID == deviceID || d.DeviceSerialNumber == deviceID {
|
||||
deviceIP = d.IPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if deviceIP == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device not found or missing IP"}); err != nil {
|
||||
log.Printf("[Mgmt] Failed to encode error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
mgr := s.sm
|
||||
s.mu.RUnlock()
|
||||
|
||||
if mgr == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "setup manager unavailable"}); err != nil {
|
||||
log.Printf("[Mgmt] Failed to encode error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
output, err := mgr.InstallSpotifyPrimer(deviceIP, targetURL)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
if encodeErr := json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error(), "output": output}); encodeErr != nil {
|
||||
log.Printf("[Mgmt] Failed to encode error: %v", encodeErr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Spotify primer installed", "output": output}); err != nil {
|
||||
log.Printf("[Mgmt] Failed to encode response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMgmtSpotifyToken returns a fresh Spotify access token and username.
|
||||
func (s *Server) HandleMgmtSpotifyToken(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.RLock()
|
||||
svc := s.spotifyService
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/setup"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func TestMgmtInstallSpotifyPrimer(t *testing.T) {
|
||||
// Prepare server with a mocked SSH to avoid real connections
|
||||
tmpDir := t.TempDir()
|
||||
ds := datastore.NewDataStore(tmpDir)
|
||||
_ = ds.Initialize()
|
||||
|
||||
s := &Server{ds: ds}
|
||||
|
||||
sm := setup.NewManager("http://localhost:8000", ds, nil)
|
||||
sm.NewSSH = func(host string) setup.SSHClient { return &mockSSH{host: host} }
|
||||
s.sm = sm
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Post("/mgmt/devices/{deviceId}/spotify/install-primer", s.HandleMgmtInstallSpotifyPrimer)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/mgmt/devices/192.168.1.10/spotify/install-primer", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if ok, _ := result["ok"].(bool); !ok {
|
||||
t.Errorf("expected ok=true, got %v", result["ok"])
|
||||
}
|
||||
if _, ok := result["output"]; !ok {
|
||||
t.Errorf("expected output field in response")
|
||||
}
|
||||
}
|
||||
@@ -1203,135 +1203,6 @@ func (m *Manager) migrateViaResolvConf(deviceIP, targetURL string) (string, erro
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// InstallSpotifyPrimer installs all components required for the Spotify boot primer on the speaker.
|
||||
func (m *Manager) InstallSpotifyPrimer(deviceIP, targetURL string) (string, error) {
|
||||
client := m.NewSSH(deviceIP)
|
||||
|
||||
var logs string
|
||||
|
||||
// 1. Create the necessary directories
|
||||
atDir := "/mnt/nv/soundtouch-service"
|
||||
_, _ = client.Run(fmt.Sprintf("mkdir -p %s", atDir))
|
||||
|
||||
// 2. Upload spotify-boot-primer script
|
||||
primerSource := "scripts/spotify/spotify-boot-primer.sh"
|
||||
|
||||
primerContent, err := os.ReadFile(primerSource)
|
||||
if err != nil {
|
||||
// Fallback for different environments (e.g. tests)
|
||||
primerSource = "../../../scripts/spotify/spotify-boot-primer.sh"
|
||||
primerContent, err = os.ReadFile(primerSource)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
remotePrimerPath := "/mnt/nv/soundtouch-service/spotify-boot-primer"
|
||||
if uploadErr := client.UploadContent(primerContent, remotePrimerPath); uploadErr != nil {
|
||||
logs += fmt.Sprintf("Warning: failed to upload %s: %v\n", remotePrimerPath, uploadErr)
|
||||
} else {
|
||||
logs += fmt.Sprintf("Uploaded %s\n", remotePrimerPath)
|
||||
_, _ = client.Run(fmt.Sprintf("chmod +x %s", remotePrimerPath))
|
||||
}
|
||||
} else {
|
||||
logs += fmt.Sprintf("Warning: could not find spotify-boot-primer.sh locally (%s): %v\n", primerSource, err)
|
||||
}
|
||||
|
||||
// 3. Create the config file
|
||||
confPath := "/mnt/nv/soundtouch-service/spotify-primer.conf"
|
||||
|
||||
confContent := fmt.Sprintf("SOUNDTOUCH_URL=%s\nSOUNDTOUCH_USER=%s\nSOUNDTOUCH_PASS=%s\n",
|
||||
targetURL, m.MgmtUsername, m.MgmtPassword)
|
||||
|
||||
if uploadErr := client.UploadContent([]byte(confContent), confPath); uploadErr != nil {
|
||||
logs += fmt.Sprintf("Warning: failed to upload %s: %v\n", confPath, uploadErr)
|
||||
} else {
|
||||
logs += fmt.Sprintf("Uploaded %s\n", confPath)
|
||||
_, _ = client.Run(fmt.Sprintf("chmod 600 %s", confPath))
|
||||
}
|
||||
|
||||
// 4. Update rc.local with the hook
|
||||
hookLogs, hookErr := m.updateRcLocalWithSpotifyHook(client)
|
||||
logs += hookLogs
|
||||
|
||||
// 5. Cleanup legacy files
|
||||
_, _ = client.Run("rm -f /mnt/nv/bin/spotify-boot-primer /mnt/nv/BoseApp-Persistence/1/spotify-primer.conf")
|
||||
|
||||
// 6. Set up .profile for PATH (optional but recommended in INSTALL.md)
|
||||
profilePath := "/mnt/nv/.profile"
|
||||
profileContent := "export PATH=\"/mnt/nv/soundtouch-service:$PATH\"\n"
|
||||
|
||||
if strings.Contains(targetURL, ".local") || strings.Contains(targetURL, "192.168.") {
|
||||
// Only add if not already there to be idempotent
|
||||
existingProfile, _ := client.Run(fmt.Sprintf("cat %s", profilePath))
|
||||
if !strings.Contains(existingProfile, "/mnt/nv/soundtouch-service") {
|
||||
newProfile := existingProfile
|
||||
if !strings.HasSuffix(newProfile, "\n") && newProfile != "" {
|
||||
newProfile += "\n"
|
||||
}
|
||||
|
||||
newProfile += profileContent
|
||||
_ = client.UploadContent([]byte(newProfile), profilePath)
|
||||
logs += "Updated /mnt/nv/.profile with PATH\n"
|
||||
}
|
||||
}
|
||||
|
||||
return logs, hookErr
|
||||
}
|
||||
|
||||
func (m *Manager) updateRcLocalWithSpotifyHook(client SSHClient) (string, error) {
|
||||
var logs string
|
||||
|
||||
rcLocalPath := "/mnt/nv/rc.local"
|
||||
primerPath := "/mnt/nv/soundtouch-service/spotify-boot-primer"
|
||||
patchStartMarker := "# --- Aftertouch Spotify hook START ---"
|
||||
patchEndMarker := "# --- Aftertouch Spotify hook END ---"
|
||||
|
||||
// Check if rc.local exists and read it
|
||||
currentRcLocal, rcErr := client.Run(fmt.Sprintf("cat %s", rcLocalPath))
|
||||
if rcErr != nil {
|
||||
currentRcLocal = ""
|
||||
}
|
||||
|
||||
if strings.Contains(currentRcLocal, patchStartMarker) {
|
||||
return fmt.Sprintf("%s already contains Spotify hook logic\n", rcLocalPath), nil
|
||||
}
|
||||
|
||||
patchLogic := fmt.Sprintf(`
|
||||
%s
|
||||
# Launches Spotify boot primer in background since SoundTouch starts at S99
|
||||
if [ -f "%s" ]; then
|
||||
%s &
|
||||
fi
|
||||
%s
|
||||
`, patchStartMarker, primerPath, primerPath, patchEndMarker)
|
||||
|
||||
newRcLocal := currentRcLocal
|
||||
// Remove "cat: can't open..." error message if it was accidentally saved in the file
|
||||
if strings.Contains(newRcLocal, "cat: can't open") {
|
||||
newRcLocal = ""
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(newRcLocal, "#!/bin/sh") {
|
||||
newRcLocal = "#!/bin/sh\n" + strings.TrimPrefix(newRcLocal, "#!/bin/sh")
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(newRcLocal, "\n") {
|
||||
newRcLocal += "\n"
|
||||
}
|
||||
|
||||
newRcLocal += patchLogic
|
||||
|
||||
if err := client.UploadContent([]byte(newRcLocal), rcLocalPath); err != nil {
|
||||
return logs, fmt.Errorf("failed to update %s: %w", rcLocalPath, err)
|
||||
}
|
||||
|
||||
logs += fmt.Sprintf("Updated %s with Spotify hook logic\n", rcLocalPath)
|
||||
|
||||
// Make it executable
|
||||
_, _ = client.Run(fmt.Sprintf("chmod +x %s", rcLocalPath))
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateRcLocalWithDNSHook(client SSHClient) (string, error) {
|
||||
var logs string
|
||||
|
||||
@@ -1517,9 +1388,6 @@ func (m *Manager) RevertMigration(deviceIP string) (string, error) {
|
||||
// 2c. Revert Aftertouch DNS Hook
|
||||
logs += m.revertAftertouchHook(client, rwCmd)
|
||||
|
||||
// 2d. Revert Spotify Primer components
|
||||
logs += m.revertSpotifyPrimer(client, rwCmd)
|
||||
|
||||
// 3. Remove CA certificate from trust store if it exists
|
||||
logs += m.revertCACert(client, rwCmd)
|
||||
|
||||
@@ -1718,59 +1586,6 @@ func (m *Manager) removeRcLocalHooks(client SSHClient, rcLocalPath string) strin
|
||||
return logs
|
||||
}
|
||||
|
||||
func (m *Manager) revertSpotifyPrimer(client SSHClient, rwCmd string) string {
|
||||
var logs string
|
||||
|
||||
// 1. Remove binary and config
|
||||
primerPath := "/mnt/nv/soundtouch-service/spotify-boot-primer"
|
||||
confPath := "/mnt/nv/soundtouch-service/spotify-primer.conf"
|
||||
legacyPrimerPath := "/mnt/nv/bin/spotify-boot-primer"
|
||||
legacyConfPath := "/mnt/nv/BoseApp-Persistence/1/spotify-primer.conf"
|
||||
|
||||
out, _ := client.Run(fmt.Sprintf("%s && rm -f %s %s %s %s", rwCmd, primerPath, confPath, legacyPrimerPath, legacyConfPath))
|
||||
if out != "" {
|
||||
logs += fmt.Sprintf("Removing primer files: %s\n", out)
|
||||
} else {
|
||||
logs += "Requested removal of Spotify primer binary and config\n"
|
||||
}
|
||||
|
||||
// 2. Remove rc.local hook (already handled by revertAftertouchHook if it uses markers,
|
||||
// but let's be explicit if we want to clean up specifically)
|
||||
// Actually revertAftertouchHook already removes blocks with "# --- Aftertouch Spotify hook START ---"
|
||||
|
||||
// 3. Optional: cleanup .profile PATH?
|
||||
// Probably better to leave it as it might contain other things, or just remove the specific line
|
||||
profilePath := "/mnt/nv/.profile"
|
||||
if content, err := client.Run(fmt.Sprintf("cat %s", profilePath)); err == nil && (strings.Contains(content, "/mnt/nv/soundtouch-service") || strings.Contains(content, "/mnt/nv/bin")) {
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
var newLines []string
|
||||
|
||||
for _, line := range lines {
|
||||
if !strings.Contains(line, "export PATH=\"/mnt/nv/soundtouch-service:$PATH\"") &&
|
||||
!strings.Contains(line, "export PATH=\"/mnt/nv/bin:$PATH\"") &&
|
||||
strings.TrimSpace(line) != "" {
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
newContent := strings.Join(newLines, "\n")
|
||||
if len(newLines) > 0 {
|
||||
newContent += "\n"
|
||||
}
|
||||
|
||||
if newContent != content {
|
||||
_ = client.UploadContent([]byte(newContent), profilePath)
|
||||
logs += "Cleaned up PATH in /mnt/nv/.profile\n"
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Cleanup consolidated directory if empty
|
||||
_, _ = client.Run("rmdir /mnt/nv/soundtouch-service 2>/dev/null")
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
func (m *Manager) revertCACert(client SSHClient, rwCmd string) string {
|
||||
var logs string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user