mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
feat: add management API endpoints matching ueberboese-app
This commit is contained in:
committed by
Tobias Gesellchen
parent
0ee673c097
commit
fced88a8a6
@@ -0,0 +1,215 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// BasicAuthMgmt returns a Basic Auth middleware using the server's management credentials.
|
||||
func (s *Server) BasicAuthMgmt() func(http.Handler) http.Handler {
|
||||
s.mu.RLock()
|
||||
username := s.mgmtUsername
|
||||
password := s.mgmtPassword
|
||||
s.mu.RUnlock()
|
||||
|
||||
return BasicAuthMiddleware(username, password)
|
||||
}
|
||||
|
||||
// HandleMgmtListSpeakers returns discovered speakers for the given account.
|
||||
func (s *Server) HandleMgmtListSpeakers(w http.ResponseWriter, r *http.Request) {
|
||||
_ = chi.URLParam(r, "accountId")
|
||||
|
||||
allDevices, err := s.ds.ListAllDevices()
|
||||
if err != nil {
|
||||
log.Printf("[Mgmt] Failed to list devices: %v", err)
|
||||
allDevices = nil
|
||||
}
|
||||
|
||||
type speaker struct {
|
||||
IPAddress string `json:"ipAddress"`
|
||||
Name string `json:"name"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
speakers := make([]speaker, 0, len(allDevices))
|
||||
for _, d := range allDevices {
|
||||
speakers = append(speakers, speaker{
|
||||
IPAddress: d.IPAddress,
|
||||
Name: d.Name,
|
||||
DeviceID: d.DeviceID,
|
||||
Type: d.ProductCode,
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"speakers": speakers,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMgmtDeviceEvents returns events for a device (currently a placeholder).
|
||||
func (s *Server) HandleMgmtDeviceEvents(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID := chi.URLParam(r, "deviceId")
|
||||
|
||||
events := s.ds.GetDeviceEvents(deviceID)
|
||||
if events == nil {
|
||||
events = nil // will marshal as empty array via wrapper
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Return the events in the structure the Flutter app expects.
|
||||
// Use an explicit empty slice to ensure JSON "[]" instead of "null".
|
||||
type eventEntry struct {
|
||||
Type string `json:"type"`
|
||||
Time string `json:"time"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
result := make([]eventEntry, 0, len(events))
|
||||
for _, e := range events {
|
||||
result = append(result, eventEntry{
|
||||
Type: e.Type,
|
||||
Time: e.Time,
|
||||
Data: e.Data,
|
||||
})
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"events": result,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMgmtSpotifyInit starts the Spotify OAuth flow by returning an authorization URL.
|
||||
func (s *Server) HandleMgmtSpotifyInit(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.RLock()
|
||||
svc := s.spotifyService
|
||||
s.mu.RUnlock()
|
||||
|
||||
if svc == nil {
|
||||
http.Error(w, `{"error":"spotify not configured"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := svc.BuildAuthorizeURL()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"redirectUrl": redirectURL,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMgmtSpotifyConfirm exchanges an authorization code for tokens.
|
||||
func (s *Server) HandleMgmtSpotifyConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
s.mu.RLock()
|
||||
svc := s.spotifyService
|
||||
s.mu.RUnlock()
|
||||
|
||||
if svc == nil {
|
||||
http.Error(w, `{"error":"spotify not configured"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
http.Error(w, `{"error":"missing code parameter"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svc.ExchangeCodeAndStore(code); err != nil {
|
||||
log.Printf("[Mgmt] Spotify confirm failed: %v", err)
|
||||
http.Error(w, `{"error":"token exchange failed"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}
|
||||
|
||||
// HandleMgmtSpotifyAccounts returns linked Spotify accounts (tokens stripped).
|
||||
func (s *Server) HandleMgmtSpotifyAccounts(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.RLock()
|
||||
svc := s.spotifyService
|
||||
s.mu.RUnlock()
|
||||
|
||||
if svc == nil {
|
||||
http.Error(w, `{"error":"spotify not configured"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
accounts := svc.GetAccounts()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"accounts": accounts,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMgmtSpotifyToken returns a fresh Spotify access token and username.
|
||||
func (s *Server) HandleMgmtSpotifyToken(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.RLock()
|
||||
svc := s.spotifyService
|
||||
s.mu.RUnlock()
|
||||
|
||||
if svc == nil {
|
||||
http.Error(w, `{"error":"spotify not configured"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, username, err := svc.GetFreshToken()
|
||||
if err != nil {
|
||||
log.Printf("[Mgmt] Spotify token error: %v", err)
|
||||
http.Error(w, `{"error":"no token available"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"access_token": accessToken,
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleMgmtSpotifyEntity resolves a Spotify URI to name and image URL.
|
||||
func (s *Server) HandleMgmtSpotifyEntity(w http.ResponseWriter, r *http.Request) {
|
||||
s.mu.RLock()
|
||||
svc := s.spotifyService
|
||||
s.mu.RUnlock()
|
||||
|
||||
if svc == nil {
|
||||
http.Error(w, `{"error":"spotify not configured"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"failed to read body"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil || req.URI == "" {
|
||||
http.Error(w, `{"error":"missing or invalid uri"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name, imageURL, err := svc.ResolveEntity(req.URI)
|
||||
if err != nil {
|
||||
log.Printf("[Mgmt] Spotify entity resolve error: %v", err)
|
||||
http.Error(w, `{"error":"entity resolution failed"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"name": name,
|
||||
"imageUrl": imageURL,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user