feat(server): stream URL gets file extension so Sonos can probe duration
This commit is contained in:
@@ -65,6 +65,10 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
|||||||
// design at
|
// design at
|
||||||
// docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md.
|
// docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md.
|
||||||
api.With(auth.OptionalUser(pool, logger)).Get("/tracks/{id}/stream", h.handleGetStream)
|
api.With(auth.OptionalUser(pool, logger)).Get("/tracks/{id}/stream", h.handleGetStream)
|
||||||
|
// Extension-bearing alias so Sonos's URL probe can identify the
|
||||||
|
// audio format from the path. The {ext} param is consumed by chi
|
||||||
|
// and ignored by the handler (which keys off {id}). See task #610.
|
||||||
|
api.With(auth.OptionalUser(pool, logger)).Get("/tracks/{id}/stream.{ext}", h.handleGetStream)
|
||||||
|
|
||||||
api.Group(func(authed chi.Router) {
|
api.Group(func(authed chi.Router) {
|
||||||
authed.Use(auth.RequireUser(pool))
|
authed.Use(auth.RequireUser(pool))
|
||||||
|
|||||||
@@ -57,6 +57,33 @@ func mimeForFormat(format string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extForFormat maps the tracks.file_format column to a path-safe file
|
||||||
|
// extension. Sonos firmware gates duration probing on the URL path
|
||||||
|
// extension (Content-Type header alone is insufficient) -- without a
|
||||||
|
// recognizable extension, Sonos reports TrackDuration=0 and seeks
|
||||||
|
// trigger auto-advance because every position past 0 looks past-the-
|
||||||
|
// end. Defaults to "mp3" for unknown formats. See task #610.
|
||||||
|
func extForFormat(format string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(format)) {
|
||||||
|
case "mp3", "mpeg":
|
||||||
|
return "mp3"
|
||||||
|
case "flac":
|
||||||
|
return "flac"
|
||||||
|
case "aac":
|
||||||
|
return "aac"
|
||||||
|
case "m4a", "mp4":
|
||||||
|
return "m4a"
|
||||||
|
case "ogg", "vorbis":
|
||||||
|
return "ogg"
|
||||||
|
case "opus":
|
||||||
|
return "opus"
|
||||||
|
case "wav", "wave":
|
||||||
|
return "wav"
|
||||||
|
default:
|
||||||
|
return "mp3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleCastStreamToken issues a short-lived HMAC stream token for the
|
// handleCastStreamToken issues a short-lived HMAC stream token for the
|
||||||
// given trackId. Authenticated via the standard session cookie / bearer.
|
// given trackId. Authenticated via the standard session cookie / bearer.
|
||||||
//
|
//
|
||||||
@@ -114,8 +141,12 @@ func (h *handlers) handleCastStreamToken(w http.ResponseWriter, r *http.Request)
|
|||||||
if h := r.Header.Get("X-Forwarded-Host"); h != "" {
|
if h := r.Header.Get("X-Forwarded-Host"); h != "" {
|
||||||
host = h
|
host = h
|
||||||
}
|
}
|
||||||
|
// Include the file extension in the path so Sonos's URL probe sees a
|
||||||
|
// recognizable audio file. Without it, Sonos reports TrackDuration=0
|
||||||
|
// and seeks past 0s land "after the end" -> early track-skip.
|
||||||
url := scheme + "://" + host + "/api/tracks/" + req.TrackID +
|
url := scheme + "://" + host + "/api/tracks/" + req.TrackID +
|
||||||
"/stream?token=" + token + "&exp=" + strconv.FormatInt(exp, 10)
|
"/stream." + extForFormat(track.FileFormat) +
|
||||||
|
"?token=" + token + "&exp=" + strconv.FormatInt(exp, 10)
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, castTokenResponse{
|
writeJSON(w, http.StatusOK, castTokenResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|||||||
Reference in New Issue
Block a user