mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
fix(bmx): serve orion at the registry-advertised path, drop the /bmx/ prefix
The BMX registry advertises orion at
`{BMX_SERVER}/core02/svc-bmx-adapter-orion/prod/orion` — no `/bmx/`
prefix. That matches the upstream Bose capture in
pkg/service/handlers/static/bmx_services_ustream.json. But our router
nested both orion routes inside the `/bmx/` chi group, so the speaker
asked `/core02/.../prod/orion/token` and our service routed
`/bmx/core02/.../prod/orion/token` — pure path mismatch. The legacy
preset URLs in issue #218 (LOCAL_INTERNET_RADIO presets pointing at
`https://content.api.bose.io/core02/svc-bmx-adapter-orion/prod/orion/station?data=...`)
also dead-ended for the same reason.
Three changes:
- Move `POST /core02/svc-bmx-adapter-orion/prod/orion/token` from the
`/bmx/` group to top level so it matches what the registry hands the
speaker.
- Add the missing `GET /core02/svc-bmx-adapter-orion/prod/orion/station`
that takes `data` as a query string. The handler reuses
bmx.PlayCustomStream — base64-decode the JSON blob (streamUrl/
imageUrl/name) and rewrap it into the standard BmxPlaybackResponse
shape, exactly the way soundcork's reference impl handles it
(soundcork main.py:786, bmx.py:720). No auth check on this endpoint:
`data` is the speaker's own preset payload, there's nothing
privileged to gate, and the upstream behaviour treats it the same way.
- Drop the local-invention `POST /bmx/orion/v1/playback/station/{data}`
route. Nothing advertised it, nothing real-world called it, and
keeping it as a "convenience alias" would have left a misleading
duplicate next to the canonical path.
TuneIn's `/bmx/tunein/...` routes stay where they are — TuneIn's
upstream baseUrl genuinely is `{BMX_SERVER}/bmx/tunein`, so the chi
group prefix is correct for that one.
Router snapshot regenerated; TestOrionPlayback flipped from
POST `/bmx/orion/v1/playback/station/{data}` to GET
`/core02/...station?data=...` (no auth header); the orion entry in
TestBMXUnauthorized's table is removed (the endpoint isn't authed
anymore, by design).
Refs #218.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2fabdece64
commit
098b4f59dd
@@ -909,10 +909,17 @@ func setupRouter(server *handlers.Server) *chi.Mux {
|
||||
r.Delete("/v1/favorite/{stationID}", server.HandleTuneInDeleteFavorite)
|
||||
})
|
||||
|
||||
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
|
||||
r.Post("/core02/svc-bmx-adapter-orion/prod/orion/token", server.HandleOrionToken)
|
||||
})
|
||||
|
||||
// Orion (LOCAL_INTERNET_RADIO) lives at the top level — the BMX registry
|
||||
// advertises baseUrl `{BMX_SERVER}/core02/svc-bmx-adapter-orion/prod/orion`
|
||||
// (no `/bmx/` prefix; verified against the upstream capture in
|
||||
// pkg/service/handlers/static/bmx_services_ustream.json), so speakers
|
||||
// reach the token + station endpoints at exactly these paths under
|
||||
// either DNS-interception or URL-flip migration.
|
||||
r.Post("/core02/svc-bmx-adapter-orion/prod/orion/token", server.HandleOrionToken)
|
||||
r.Get("/core02/svc-bmx-adapter-orion/prod/orion/station", server.HandleOrionPlayback)
|
||||
|
||||
r.Get("/custom/v1/playback/{encodedURL}", server.HandleCustomPlayback)
|
||||
|
||||
r.Route("/streaming", func(r chi.Router) {
|
||||
|
||||
+2
-2
@@ -30,6 +30,7 @@ GET /bmx/tunein/v1/playback/episodes/{podcastID} handlers.(
|
||||
GET /bmx/tunein/v1/playback/station/{stationID} handlers.(*Server).HandleTuneInPlayback-fm
|
||||
GET /bmx/tunein/v1/search handlers.(*Server).HandleTuneInSearch-fm
|
||||
GET /ced/* handlers.(*Server).HandleCedStatic
|
||||
GET /core02/svc-bmx-adapter-orion/prod/orion/station handlers.(*Server).HandleOrionPlayback-fm
|
||||
GET /custom/v1/playback/{encodedURL} handlers.(*Server).HandleCustomPlayback-fm
|
||||
GET /customer/account/{account} handlers.(*Server).HandleMargeAccountProfile-fm
|
||||
GET /docs/* handlers.(*Server).HandleDocs-fm
|
||||
@@ -97,11 +98,10 @@ POST /accounts/{account}/group handlers.(
|
||||
POST /accounts/{account}/group/ handlers.(*Server).HandleMargeAddGroup-fm
|
||||
POST /accounts/{account}/group/{groupId} handlers.(*Server).HandleMargeModifyGroup-fm
|
||||
POST /alexa/certificate handlers.(*Server).HandleAlexaCertificate-fm
|
||||
POST /bmx/core02/svc-bmx-adapter-orion/prod/orion/token handlers.(*Server).HandleOrionToken-fm
|
||||
POST /bmx/orion/v1/playback/station/{data} handlers.(*Server).HandleOrionPlayback-fm
|
||||
POST /bmx/tunein/v1/favorite/{stationID} handlers.(*Server).HandleTuneInFavorite-fm
|
||||
POST /bmx/tunein/v1/report handlers.(*Server).HandleTuneInReport-fm
|
||||
POST /bmx/tunein/v1/token handlers.(*Server).HandleTuneInToken-fm
|
||||
POST /core02/svc-bmx-adapter-orion/prod/orion/token handlers.(*Server).HandleOrionToken-fm
|
||||
POST /customer/account/{account} handlers.(*Server).HandleMargeUpdateAccountProfile-fm
|
||||
POST /customer/account/{account}/password handlers.(*Server).HandleMargeChangePassword-fm
|
||||
POST /mgmt/accounts/{accountId}/language handlers.(*Server).HandleMgmtUpdateAccountLanguage-fm
|
||||
|
||||
@@ -173,14 +173,20 @@ func (s *Server) HandleOrionToken(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleOrionPlayback returns Orion playback information.
|
||||
// HandleOrionPlayback returns Orion playback information for the
|
||||
// /core02/svc-bmx-adapter-orion/prod/orion/station?data=... endpoint
|
||||
// the speaker reaches by following its stored LOCAL_INTERNET_RADIO
|
||||
// preset's `location` attribute. The `data` query string is the
|
||||
// base64-encoded JSON blob (streamUrl/imageUrl/name) that the speaker
|
||||
// constructed when the preset was first saved; we just decode and
|
||||
// rewrap it into the Bose BmxPlaybackResponse shape via
|
||||
// bmx.PlayCustomStream.
|
||||
//
|
||||
// No auth check: the data is the speaker's own input, nothing
|
||||
// privileged is being protected, and soundcork's reference impl
|
||||
// behaves the same way.
|
||||
func (s *Server) HandleOrionPlayback(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
s.writeBMXUnauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
data := chi.URLParam(r, "data")
|
||||
data := r.URL.Query().Get("data")
|
||||
|
||||
resp, err := bmx.PlayCustomStream(data)
|
||||
if err != nil {
|
||||
|
||||
@@ -87,8 +87,13 @@ func TestOrionPlayback(t *testing.T) {
|
||||
// Base64 encoded: {"streamUrl": "http://example.com/stream", "imageUrl": "http://example.com/img.jpg", "name": "Test Orion"}
|
||||
data := "eyJzdHJlYW1VcmwiOiAiaHR0cDovL2V4YW1wbGUuY29tL3N0cmVhbSIsICJpbWFnZVVybCI6ICJodHRwOi8vZXhhbXBsZS5jb20vaW1nLmpwZyIsICJuYW1lIjogIlRlc3QgT3Jpb24ifQ=="
|
||||
|
||||
req, _ := http.NewRequest("POST", ts.URL+"/bmx/orion/v1/playback/station/"+data, nil)
|
||||
req.Header.Set("Authorization", "Bearer mock-token")
|
||||
// Speakers reach this endpoint by following the `location` attribute
|
||||
// stored in a LOCAL_INTERNET_RADIO preset's contentItem — a GET to
|
||||
// the upstream path with `data` as a query string. The data is
|
||||
// already base64-URL-safe; passing it raw mirrors what the speaker
|
||||
// emits (Go's url package re-encodes any `=` padding for transport).
|
||||
req, _ := http.NewRequest("GET",
|
||||
ts.URL+"/core02/svc-bmx-adapter-orion/prod/orion/station?data="+url.QueryEscape(data), nil)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -165,7 +170,11 @@ func TestBMXUnauthorized(t *testing.T) {
|
||||
{"GET", "/bmx/tunein/v1/playback/station/s123"},
|
||||
{"GET", "/bmx/tunein/v1/playback/episodes/p123"},
|
||||
{"GET", "/bmx/tunein/v1/playback/episode/p123"},
|
||||
{"POST", "/bmx/orion/v1/playback/station/data"},
|
||||
// Note: /core02/.../prod/orion/station is intentionally NOT in this
|
||||
// list — orion playback takes its `data` blob from the speaker's
|
||||
// own preset payload, there's no privileged material to gate, and
|
||||
// soundcork's reference impl makes the same call (no auth on the
|
||||
// station endpoint). See HandleOrionPlayback's doc comment.
|
||||
}
|
||||
|
||||
for _, tc := range paths {
|
||||
|
||||
@@ -32,9 +32,14 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
|
||||
r.Get("/tunein/v1/navigate", server.HandleTuneInNavigate)
|
||||
r.Get("/tunein/v1/navigate/*", server.HandleTuneInNavigate)
|
||||
r.Get("/tunein/v1/search", server.HandleTuneInSearch)
|
||||
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
|
||||
})
|
||||
|
||||
// Orion lives at the top level — see the matching note in
|
||||
// cmd/soundtouch-service/main.go. Mirrored here so the test router
|
||||
// exercises the same paths the production router does.
|
||||
r.Post("/core02/svc-bmx-adapter-orion/prod/orion/token", server.HandleOrionToken)
|
||||
r.Get("/core02/svc-bmx-adapter-orion/prod/orion/station", server.HandleOrionPlayback)
|
||||
|
||||
r.Get("/custom/v1/playback/{encodedURL}", server.HandleCustomPlayback)
|
||||
|
||||
streamingRoutes := func(r chi.Router) {
|
||||
|
||||
Reference in New Issue
Block a user