package subsonic import ( "encoding/json" "encoding/xml" "fmt" "io" "net/http" "strings" ) // 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" ) // ServerVersion identifies this build. Overwritten by cmd/minstrel at startup. var ServerVersion = "dev" // 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: ServerVersion, 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) }