refactor(server): unify stream URL builders + MIME tables + cover-path helper
test-go / test (push) Successful in 28s
test-go / integration (push) Has been cancelled

Closes Scribe #614, #615, server half of #616 surfaced by the 2026-06-04 divergent-provider audit.

- streamURL helper now used everywhere /api/tracks/{id}/stream is built (was inline concat in playlists.go and cast_token.go); add streamURLWithExt for the .ext cast variant.

- audioContentType in media.go is the canonical file_format -> MIME lookup; mimeForFormat in cast_token.go is now a thin wrapper that overrides the unknown-format fallback to audio/mpeg (Sonos rejects octet-stream). Adds mpeg/vorbis/wave aliases. Subsonic's contentTypeForFormat stays frozen per docs.

- coverart.ResolveAlbumPath extracted; api and subsonic both delegate to it.
This commit is contained in:
2026-06-04 08:29:51 -04:00
parent edd198cdf5
commit 024493f2a7
6 changed files with 69 additions and 62 deletions
+10 -21
View File
@@ -33,28 +33,18 @@ type castTokenResponse struct {
Title string `json:"title"`
}
// mimeForFormat maps the tracks.file_format column to an HTTP audio
// MIME type. Sonos requires the protocolInfo MIME on DIDL-Lite to match
// what the URL actually serves; "audio/*" wildcard is silently rejected.
// Unknown formats fall back to audio/mpeg — most Sonos firmware probes
// the URL anyway and recovers from a small MIME mismatch.
// mimeForFormat returns the audio MIME type for a cast (Sonos/UPnP) URL.
// Wraps the canonical audioContentType lookup in media.go and overrides
// the unknown-format fallback to audio/mpeg, because Sonos rejects
// DIDL-Lite with protocolInfo=application/octet-stream (the browser
// fallback) -- most Sonos firmware probes the URL anyway and recovers
// from a small MIME mismatch.
func mimeForFormat(format string) string {
switch strings.ToLower(strings.TrimSpace(format)) {
case "mp3", "mpeg":
return "audio/mpeg"
case "flac":
return "audio/flac"
case "aac", "m4a", "mp4":
return "audio/mp4"
case "ogg", "vorbis":
return "audio/ogg"
case "opus":
return "audio/opus"
case "wav", "wave":
return "audio/wav"
default:
mime := audioContentType(format)
if mime == "application/octet-stream" {
return "audio/mpeg"
}
return mime
}
// extForFormat maps the tracks.file_format column to a path-safe file
@@ -144,8 +134,7 @@ func (h *handlers) handleCastStreamToken(w http.ResponseWriter, r *http.Request)
// 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 +
"/stream." + extForFormat(track.FileFormat) +
url := scheme + "://" + host + streamURLWithExt(trackUUID, extForFormat(track.FileFormat)) +
"?token=" + token + "&exp=" + strconv.FormatInt(exp, 10)
writeJSON(w, http.StatusOK, castTokenResponse{