Files
minstrel/internal/subsonic/envelope.go
T
bvandeusenandClaude Opus 5.5 c1305a547d
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m35s
release / go (push) Successful in 2m0s
release / integration (push) Successful in 5m40s
release / android (push) Successful in 6m15s
release / Build signed APK (releases and dev) (push) Successful in 6m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
feat: the admin Overview shows who is listening, downloads, the library and its disk, and the server's version, uptime and recent errors; a download waiting on you, a client not answering or a full disk count in its verdict (M504 #5627 #5628 #5629 #5630)
The version moves to internal/buildinfo, so /rest/ping reports the build
instead of "dev".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:51:41 -04:00

98 lines
3.1 KiB
Go

package subsonic
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"strings"
"git.fabledsword.com/bvandeusen/minstrel/internal/buildinfo"
)
// APIVersion is the Subsonic REST version we advertise. 1.16.1 is the last
// "classic" level; OpenSubsonic extensions are surfaced via the openSubsonic
// flag and the type/serverVersion fields.
const (
APIVersion = "1.16.1"
ServerType = "minstrel"
XMLNS = "http://subsonic.org/restapi"
)
// Envelope holds the attributes present on every Subsonic response. Concrete
// response types embed it and add endpoint-specific fields with json+xml tags.
type Envelope struct {
XMLName xml.Name `json:"-" xml:"subsonic-response"`
XMLNS string `json:"-" xml:"xmlns,attr"`
Status string `json:"status" xml:"status,attr"`
Version string `json:"version" xml:"version,attr"`
Type string `json:"type" xml:"type,attr"`
ServerVersion string `json:"serverVersion" xml:"serverVersion,attr"`
OpenSubsonic bool `json:"openSubsonic" xml:"openSubsonic,attr"`
Error *Error `json:"error,omitempty" xml:"error,omitempty"`
}
// Error is the body of a failed response.
type Error struct {
XMLName xml.Name `json:"-" xml:"error"`
Code int `json:"code" xml:"code,attr"`
Message string `json:"message" xml:"message,attr"`
}
// NewEnvelope returns a populated header for the given status ("ok"|"failed").
func NewEnvelope(status string) Envelope {
return Envelope{
XMLNS: XMLNS,
Status: status,
Version: APIVersion,
Type: ServerType,
ServerVersion: buildinfo.Version,
OpenSubsonic: true,
}
}
// Write renders body in the format requested by the ?f= query parameter.
// body must embed Envelope. Unknown formats fall back to JSON.
func Write(w http.ResponseWriter, r *http.Request, body any) {
switch strings.ToLower(r.URL.Query().Get("f")) {
case "xml":
writeXML(w, body)
case "jsonp":
writeJSONP(w, body, r.URL.Query().Get("callback"))
default:
writeJSON(w, body)
}
}
// WriteFail emits an envelope-only failed response with the given error code.
func WriteFail(w http.ResponseWriter, r *http.Request, code int, message string) {
env := NewEnvelope("failed")
env.Error = &Error{Code: code, Message: message}
Write(w, r, env)
}
func writeJSON(w http.ResponseWriter, body any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"subsonic-response": body})
}
func writeJSONP(w http.ResponseWriter, body any, callback string) {
if callback == "" {
// Per spec the client must supply callback; fall back to plain JSON
// rather than producing invalid JavaScript.
writeJSON(w, body)
return
}
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
_, _ = fmt.Fprintf(w, "%s(", callback)
_ = json.NewEncoder(w).Encode(map[string]any{"subsonic-response": body})
_, _ = io.WriteString(w, ")")
}
func writeXML(w http.ResponseWriter, body any) {
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
_, _ = io.WriteString(w, xml.Header)
_ = xml.NewEncoder(w).Encode(body)
}