Add more routes to be handled by ourselves

Group management is only implemented as placeholder
This commit is contained in:
Tobias Gesellchen
2026-02-22 20:48:42 +01:00
parent f50ee1131e
commit b71a3830ec
4 changed files with 437 additions and 72 deletions
+45 -33
View File
@@ -606,40 +606,57 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
streamingRoutes := func(r chi.Router) {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
r.Post("/account/{account}/device/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/support/power_on", server.HandleMargePowerOn)
r.Get("/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Get("/account/{account}/device/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/account/{account}/device/{device}/group/member", server.HandleMargeDeviceGroupMember)
r.Post("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/account/{account}/full", server.HandleMargeAccountFull)
r.Get("/software/update/account/{account}", server.HandleMargeSoftwareUpdate)
r.Route("/stats", func(r chi.Router) {
r.Post("/usage", server.HandleUsageStats)
r.Post("/error", server.HandleErrorStats)
})
}
accountsRoutes := func(r chi.Router) {
r.Get("/{account}/full", server.HandleMargeAccountFull)
r.Get("/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Get("/{account}/devices/{device}/recents", server.HandleMargeRecents)
r.Post("/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/{account}/devices/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/{account}/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
}
r.Route("/marge", func(r chi.Router) {
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
r.Post("/streaming/support/power_on", server.HandleMargePowerOn)
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Get("/accounts/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/accounts/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/accounts/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/accounts/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/accounts/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/streaming/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/streaming/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Post("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
})
// Legacy or direct domain calls without /marge prefix
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
r.Post("/streaming/support/power_on", server.HandleMargePowerOn)
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Get("/accounts/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/accounts/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/accounts/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/accounts/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/accounts/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/streaming/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/streaming/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Post("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Route("/customer", func(r chi.Router) {
r.Get("/account/{account}", server.HandleMargeAccountProfile)
@@ -652,11 +669,6 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Post("/scmudc/{deviceId}", server.HandleAppEvents)
})
r.Route("/streaming/stats", func(r chi.Router) {
r.Post("/usage", server.HandleUsageStats)
r.Post("/error", server.HandleErrorStats)
})
r.Route("/mgmt", func(r chi.Router) {
// Browser OAuth callback — no auth required (Spotify redirects the
// user's browser here directly). The authorization code is single-use,
+59 -10
View File
@@ -28,7 +28,7 @@ func (s *Server) HandleMargeSourceProviders(w http.ResponseWriter, r *http.Reque
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.Header()["ETag"] = []string{etag}
_, _ = w.Write(data)
}
@@ -51,7 +51,7 @@ func (s *Server) HandleMargeAccountFull(w http.ResponseWriter, r *http.Request)
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.Header()["ETag"] = []string{etag}
_, _ = w.Write(data)
}
@@ -146,7 +146,7 @@ func (s *Server) HandleMargeGetEmailAddress(w http.ResponseWriter, _ *http.Reque
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_, _ = w.Write([]byte(xml.Header))
_, _ = w.Write(data)
}
@@ -165,7 +165,7 @@ func (s *Server) HandleMargeGetDeviceSettings(w http.ResponseWriter, _ *http.Req
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_, _ = w.Write([]byte(xml.Header))
_, _ = w.Write(data)
}
@@ -184,9 +184,16 @@ func (s *Server) HandleMargeSoftwareUpdate(w http.ResponseWriter, r *http.Reques
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.Header()["ETag"] = []string{etag}
// For the account-specific firmware route, always return the software_update tag.
// This route is specifically used by firmware like Bose_Lisa/27.0.6.
if chi.URLParam(r, "account") != "" {
_, _ = w.Write([]byte(marge.SoftwareUpdateToXML()))
return
}
if len(swUpdateXML) > 0 {
_, _ = w.Write(swUpdateXML)
} else {
@@ -211,7 +218,7 @@ func (s *Server) HandleMargePresets(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.Header()["ETag"] = []string{etag}
_, _ = w.Write(data)
}
@@ -244,7 +251,29 @@ func (s *Server) HandleMargeUpdatePreset(w http.ResponseWriter, r *http.Request)
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_, _ = w.Write(data)
}
// HandleMargeRecents returns the Marge recents for a device.
func (s *Server) HandleMargeRecents(w http.ResponseWriter, r *http.Request) {
account := chi.URLParam(r, "account")
device := chi.URLParam(r, "device")
etag := strconv.FormatInt(s.ds.GetETagForRecents(account, device), 10)
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
data, err := marge.RecentsToXML(s.ds, account, device)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.Header()["ETag"] = []string{etag}
_, _ = w.Write(data)
}
@@ -268,7 +297,7 @@ func (s *Server) HandleMargeAddRecent(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_, _ = w.Write(data)
}
@@ -288,7 +317,7 @@ func (s *Server) HandleMargeAddDevice(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_, _ = w.Write(data)
}
@@ -310,7 +339,7 @@ func (s *Server) HandleMargeRemoveDevice(w http.ResponseWriter, r *http.Request)
func (s *Server) HandleMargeProviderSettings(w http.ResponseWriter, r *http.Request) {
account := chi.URLParam(r, "account")
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_, _ = w.Write([]byte(marge.ProviderSettingsToXML(account)))
}
@@ -336,6 +365,26 @@ func (s *Server) HandleMargeStreamingToken(w http.ResponseWriter, _ *http.Reques
_, _ = w.Write(data)
}
// HandleMargeDeviceGroup returns grouping information for a device (empty group by default).
func (s *Server) HandleMargeDeviceGroup(w http.ResponseWriter, _ *http.Request) {
// Native firmware expects vnd.bose.streaming content type
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><group/>`))
}
// HandleMargeDeviceGroupServer returns grouping server information (404 by default if not a server).
func (s *Server) HandleMargeDeviceGroupServer(w http.ResponseWriter, r *http.Request) {
// Not in a group as server
http.NotFound(w, r)
}
// HandleMargeDeviceGroupMember returns grouping member information (404 by default if not a member).
func (s *Server) HandleMargeDeviceGroupMember(w http.ResponseWriter, r *http.Request) {
// Not in a group as member
http.NotFound(w, r)
}
// HandleMargeCustomerSupport handles Marge customer support uploads.
func (s *Server) HandleMargeCustomerSupport(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
+293 -1
View File
@@ -264,7 +264,7 @@ func TestMargeUpdatePreset(t *testing.T) {
}
}
func TestMargeDeviceInfo(t *testing.T) {
func TestMargeAddRecentRoute(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
@@ -331,6 +331,298 @@ func TestMargeDeviceInfo(t *testing.T) {
}
}
func TestMargeNativeStreamingRoutes(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-test-native-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tempDir) }()
ds := datastore.NewDataStore(tempDir)
account := "12345"
deviceID := "DEV1"
accountDir := filepath.Join(tempDir, "accounts", account)
deviceDir := filepath.Join(accountDir, "devices", deviceID)
err = os.MkdirAll(deviceDir, 0755)
if err != nil {
t.Fatalf("Failed to create device dir: %v", err)
}
// Mock Sources.xml for recent tests
if err := os.WriteFile(filepath.Join(deviceDir, "Sources.xml"), []byte(`
<sources>
<source id="SRC1" displayName="TUNEIN" secret="" secretType="Audio">
<sourceKey type="TUNEIN" account=""/>
</source>
</sources>
`), 0644); err != nil {
t.Fatalf("Failed to write Sources.xml: %v", err)
}
if err := os.WriteFile(filepath.Join(deviceDir, "Recents.xml"), []byte(`<recents></recents>`), 0644); err != nil {
t.Fatalf("Failed to write Recents.xml: %v", err)
}
if err := os.WriteFile(filepath.Join(deviceDir, "Presets.xml"), []byte(`<presets></presets>`), 0644); err != nil {
t.Fatalf("Failed to write Presets.xml: %v", err)
}
r, _ := setupRouter("http://localhost:8001", ds)
ts := httptest.NewServer(r)
defer ts.Close()
t.Run("POST /streaming/account/{account}/device/{device}/recent", func(t *testing.T) {
payload := `
<recent>
<name>New Route Recent</name>
<sourceid>SRC1</sourceid>
<location>/station/s999</location>
<contentItemType>station</contentItemType>
</recent>`
res, err := http.Post(ts.URL+"/streaming/account/"+account+"/device/"+deviceID+"/recent", "application/xml", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
// Verify file was saved
recentData, _ := os.ReadFile(filepath.Join(deviceDir, "Recents.xml"))
if !strings.Contains(string(recentData), "New Route Recent") {
t.Error("Recent from native route was not saved to datastore")
}
})
t.Run("GET /streaming/account/{account}/full", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/account/" + account + "/full")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
fullData, _ := io.ReadAll(res.Body)
if !strings.Contains(string(fullData), account) {
t.Error("Account full response does not contain account ID")
}
})
t.Run("GET /streaming/software/update/account/{account}", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/software/update/account/" + account)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
swData, _ := io.ReadAll(res.Body)
if !strings.Contains(string(swData), "software_update") {
t.Errorf("Response missing software_update tag: %s", string(swData))
}
})
t.Run("GET /streaming/account/{account}/device/{device}/recent", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/account/" + account + "/device/" + deviceID + "/recent")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
etag := res.Header.Get("ETag")
if etag == "" {
t.Error("Expected ETag header")
}
recentData, _ := io.ReadAll(res.Body)
if !strings.Contains(string(recentData), "recents") {
t.Errorf("Response missing recents tag: %s", string(recentData))
}
// Test 304
req, _ := http.NewRequest("GET", ts.URL+"/streaming/account/"+account+"/device/"+deviceID+"/recent", nil)
req.Header.Set("If-None-Match", etag)
res2, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer res2.Body.Close()
if res2.StatusCode != http.StatusNotModified {
t.Errorf("Expected 304 Not Modified, got %v", res2.Status)
}
})
t.Run("GET /streaming/account/{account}/device/{device}/presets", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/account/" + account + "/device/" + deviceID + "/presets")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
etag := res.Header.Get("ETag")
if etag == "" {
t.Error("Expected ETag header")
}
presetData, _ := io.ReadAll(res.Body)
if !strings.Contains(string(presetData), "presets") {
t.Errorf("Response missing presets tag: %s", string(presetData))
}
// Test 304
req, _ := http.NewRequest("GET", ts.URL+"/streaming/account/"+account+"/device/"+deviceID+"/presets", nil)
req.Header.Set("If-None-Match", etag)
res2, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer res2.Body.Close()
if res2.StatusCode != http.StatusNotModified {
t.Errorf("Expected 304 Not Modified, got %v", res2.Status)
}
})
t.Run("POST /streaming/account/{account}/device/{device}/presets/{presetNumber}", func(t *testing.T) {
payload := `
<preset>
<name>New Native Preset</name>
<sourceid>SRC1</sourceid>
<location>/station/s777</location>
<contentItemType>station</contentItemType>
</preset>`
res, err := http.Post(ts.URL+"/streaming/account/"+account+"/device/"+deviceID+"/presets/1", "application/xml", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
// Verify file was saved
presetData, _ := os.ReadFile(filepath.Join(deviceDir, "Presets.xml"))
if !strings.Contains(string(presetData), "New Native Preset") {
t.Error("Preset from native route was not saved to datastore")
}
})
t.Run("GET /streaming/account/{account}/device/{device}/group/", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/account/" + account + "/device/" + deviceID + "/group/")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
groupData, _ := io.ReadAll(res.Body)
if !strings.Contains(string(groupData), "<group") {
t.Errorf("Response missing group tag: %s", string(groupData))
}
})
t.Run("GET /streaming/account/{account}/device/{device}/group/server", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/account/" + account + "/device/" + deviceID + "/group/server")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusNotFound {
t.Errorf("Expected 404 Not Found, got %v", res.Status)
}
})
t.Run("GET /streaming/account/{account}/device/{device}/group/member", func(t *testing.T) {
res, err := http.Get(ts.URL + "/streaming/account/" + account + "/device/" + deviceID + "/group/member")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusNotFound {
t.Errorf("Expected 404 Not Found, got %v", res.Status)
}
})
t.Run("GET /marge/accounts/{account}/devices/{device}/group", func(t *testing.T) {
res, err := http.Get(ts.URL + "/marge/accounts/" + account + "/devices/" + deviceID + "/group")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
t.Errorf("Expected status OK, got %v: %s", res.Status, string(body))
}
if ct := res.Header.Get("Content-Type"); ct != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", ct)
}
groupData, _ := io.ReadAll(res.Body)
if !strings.Contains(string(groupData), "<group") {
t.Errorf("Response missing group tag: %s", string(groupData))
}
})
}
func TestMargeAddRemoveDevice(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-test-*")
if err != nil {
+40 -28
View File
@@ -36,41 +36,53 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
streamingRoutes := func(r chi.Router) {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
r.Post("/account/{account}/device/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/support/power_on", server.HandleMargePowerOn)
r.Get("/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
// Native group endpoint (both with and without trailing slash)
r.Get("/account/{account}/device/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/account/{account}/device/{device}/group/member", server.HandleMargeDeviceGroupMember)
r.Post("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/account/{account}/full", server.HandleMargeAccountFull)
r.Get("/software/update/account/{account}", server.HandleMargeSoftwareUpdate)
}
accountsRoutes := func(r chi.Router) {
r.Get("/{account}/full", server.HandleMargeAccountFull)
r.Get("/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Get("/{account}/devices/{device}/recents", server.HandleMargeRecents)
r.Post("/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/{account}/devices/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/{account}/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
}
// Setup Marge for tests
r.Route("/marge", func(r chi.Router) {
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
r.Post("/streaming/support/power_on", server.HandleMargePowerOn)
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Get("/accounts/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/accounts/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/accounts/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/accounts/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/accounts/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/streaming/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/streaming/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Post("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
})
// Legacy or direct domain calls without /marge prefix
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
r.Post("/streaming/support/power_on", server.HandleMargePowerOn)
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Get("/accounts/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/accounts/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/accounts/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/accounts/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/accounts/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/streaming/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/streaming/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Post("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
// Setup Customer for tests
r.Route("/customer", func(r chi.Router) {