99 lines
3.1 KiB
Go
99 lines
3.1 KiB
Go
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)
|
|
}
|