feat(bmx): SiriusXM live-adapter logging stub

bmx_services.json advertises SIRIUSXM_EVEREST at
`{BMX_SERVER}/core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter`
and bmx_services_availability.json lists it as available, so speakers
that try SiriusXM hit that path. Without a route we 404'd silently
and the call was invisible in our logs.

  - HandleSiriusXMLiveAdapter at the bare base URL returns the
    SIRIUSXM_EVEREST service descriptor (selected by id.name from
    bmx_services.json, with {BMX_SERVER}/{MEDIA_SERVER} substitution).
    Mirrors deborahgu/soundcork main.py:805 in shape.

  - HandleSiriusXMLiveAdapterSubpath catches every sub-path advertised
    by the descriptor's _links (/availability, /token, /navigate,
    /logout) plus the playback URLs the speaker discovers via navigate.
    Logs the request with method+path+UA+Authorization+RawQuery, then
    404s — giving the next implementation pass concrete data about
    what the speaker actually asks for.

Two helpers added to handlers_bmx.go (shared with any future
BMX-segment stub):

  - extractBMXService(json, name) — find a service entry by id.name.
  - (*Server).applyBMXTemplate(content) — {BMX_SERVER}/{MEDIA_SERVER}
    substitution, identical to what HandleBMXRegistry does inline.

Routes registered next to Orion at the top level — same convention
(no /bmx/ prefix) because bmx_services.json advertises baseUrl without
that prefix and speakers reach the path verbatim under either
migration mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-17 15:05:39 +02:00
co-authored by Claude Opus 4.7
parent b9c1cdad29
commit 2df0adf4e3
3 changed files with 119 additions and 0 deletions
+56
View File
@@ -12,6 +12,8 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
@@ -43,6 +45,60 @@ func (s *Server) HandleBMXServicesAvailability(w http.ResponseWriter, _ *http.Re
_, _ = w.Write(bmxServicesAvailabilityJSON)
}
// extractBMXService finds a single service entry in bmx_services.json by
// its `id.name` (e.g. "SIRIUSXM_EVEREST", "TUNEIN"). Returns the raw JSON
// segment for that service so callers can apply {BMX_SERVER} / {MEDIA_SERVER}
// substitution and write it back to the wire.
func extractBMXService(bmxJSON []byte, name string) (json.RawMessage, error) {
var wrapper struct {
BMXServices []json.RawMessage `json:"bmx_services"`
}
if err := json.Unmarshal(bmxJSON, &wrapper); err != nil {
return nil, fmt.Errorf("parse bmx_services.json: %w", err)
}
for _, raw := range wrapper.BMXServices {
var idOnly struct {
ID struct {
Name string `json:"name"`
} `json:"id"`
}
if err := json.Unmarshal(raw, &idOnly); err != nil {
continue
}
if idOnly.ID.Name == name {
return raw, nil
}
}
return nil, fmt.Errorf("service %q not found in bmx_services.json", name)
}
// applyBMXTemplate runs the same {BMX_SERVER} / {MEDIA_SERVER} substitution
// HandleBMXRegistry uses, so service-descriptor responses produced from
// sub-segments of bmx_services.json land at the same hostnames the
// registry advertises.
func (s *Server) applyBMXTemplate(content string) string {
baseURL := s.serverURL
s.mu.RLock()
dnsEnabled := s.dnsEnabled
s.mu.RUnlock()
bmxServer := baseURL
if dnsEnabled {
bmxServer = "https://content.api.bose.io"
}
content = strings.ReplaceAll(content, "{BMX_SERVER}", bmxServer)
content = strings.ReplaceAll(content, "{MEDIA_SERVER}", baseURL+"/media")
return content
}
// writeBMXUnauthorized writes the canonical 401 used by every BMX adapter
// handler that requires an Authorization header (TuneIn variants, Orion
// playback). Currently unused because all gate sites are temporarily
@@ -0,0 +1,53 @@
// Package handlers — SiriusXM BMX adapter (logging stub).
//
// bmx_services.json declares SIRIUSXM_EVEREST at
// `{BMX_SERVER}/core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter`,
// and bmx_services_availability.json lists it as available — so speakers
// that try SiriusXM hit this path. The bare URL returns the service
// descriptor; sub-paths advertised by the descriptor's _links
// (/availability, /token, /navigate, /logout, plus the playback paths
// the speaker discovers via navigate) currently log + 404 so we have
// visibility into real speaker calls for the next implementation pass.
//
// Reference: deborahgu/soundcork main.py:805 takes the same shape —
// returns the SiriusXM service descriptor from the BMX services array
// (hardcoded index 2). We select by id.name instead of array index.
package handlers
import (
"log"
"net/http"
)
// HandleSiriusXMLiveAdapter returns the SIRIUSXM_EVEREST service descriptor
// from bmx_services.json for the bare live-adapter base URL.
func (s *Server) HandleSiriusXMLiveAdapter(w http.ResponseWriter, r *http.Request) {
log.Printf("[BMX SiriusXM] %s %s ua=%q auth=%q query=%q",
r.Method, r.URL.Path, r.UserAgent(),
r.Header.Get("Authorization"), r.URL.RawQuery)
svc, err := extractBMXService(bmxServicesJSON, "SIRIUSXM_EVEREST")
if err != nil {
log.Printf("[BMX SiriusXM] failed to extract service descriptor: %v", err)
http.Error(w, "service descriptor unavailable", http.StatusInternalServerError)
return
}
body := s.applyBMXTemplate(string(svc))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(body))
}
// HandleSiriusXMLiveAdapterSubpath logs and 404s any unimplemented sub-path
// under the SiriusXM live-adapter. Visibility for the next implementation
// pass — the _links in the descriptor publish /availability, /token,
// /navigate, /logout; playback URLs come dynamically from navigate.
func (s *Server) HandleSiriusXMLiveAdapterSubpath(w http.ResponseWriter, r *http.Request) {
log.Printf("[BMX SiriusXM] UNIMPLEMENTED %s %s ua=%q auth=%q query=%q",
r.Method, r.URL.Path, r.UserAgent(),
r.Header.Get("Authorization"), r.URL.RawQuery)
http.Error(w, "not implemented", http.StatusNotFound)
}