Send Subsonic formatted response on marshalling errors

This commit is contained in:
Deluan 2024-02-17 10:39:29 -05:00
parent 97c7e5daaf
commit 176329343a
7 changed files with 71 additions and 52 deletions

View File

@ -39,12 +39,14 @@ var _ = Describe("Helpers", func() {
m := &Model{ID: "123", AlbumId: "456", CreatedAt: now, UpdatedAt: &now, PlayCount: 2} m := &Model{ID: "123", AlbumId: "456", CreatedAt: now, UpdatedAt: &now, PlayCount: 2}
args, err := toSQLArgs(m) args, err := toSQLArgs(m)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(args).To(HaveKeyWithValue("id", "123")) Expect(args).To(SatisfyAll(
Expect(args).To(HaveKeyWithValue("album_id", "456")) HaveKeyWithValue("id", "123"),
Expect(args).To(HaveKeyWithValue("play_count", 2)) HaveKeyWithValue("album_id", "456"),
Expect(args).To(HaveKeyWithValue("updated_at", now.Format(time.RFC3339Nano))) HaveKeyWithValue("play_count", 2),
Expect(args).To(HaveKeyWithValue("created_at", now.Format(time.RFC3339Nano))) HaveKeyWithValue("updated_at", now.Format(time.RFC3339Nano)),
Expect(args).ToNot(HaveKey("Embed")) HaveKeyWithValue("created_at", now.Format(time.RFC3339Nano)),
Not(HaveKey("Embed")),
))
}) })
}) })

View File

@ -31,13 +31,15 @@ Input #0, ape, from './Capture/02 01 - Symphony No. 5 in C minor, Op. 67 I. Alle
CatalogNumber : PLD 1201 CatalogNumber : PLD 1201
` `
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md).To(HaveKeyWithValue("catalognumber", []string{"PLD 1201"})) Expect(md).To(SatisfyAll(
Expect(md).To(HaveKeyWithValue("musicbrainz_trackid", []string{"ffe06940-727a-415a-b608-b7e45737f9d8"})) HaveKeyWithValue("catalognumber", []string{"PLD 1201"}),
Expect(md).To(HaveKeyWithValue("musicbrainz_albumid", []string{"71eb5e4a-90e2-4a31-a2d1-a96485fcb667"})) HaveKeyWithValue("musicbrainz_trackid", []string{"ffe06940-727a-415a-b608-b7e45737f9d8"}),
Expect(md).To(HaveKeyWithValue("musicbrainz_artistid", []string{"1f9df192-a621-4f54-8850-2c5373b7eac9"})) HaveKeyWithValue("musicbrainz_albumid", []string{"71eb5e4a-90e2-4a31-a2d1-a96485fcb667"}),
Expect(md).To(HaveKeyWithValue("musicbrainz_albumartistid", []string{"89ad4ac3-39f7-470e-963a-56509c546377"})) HaveKeyWithValue("musicbrainz_artistid", []string{"1f9df192-a621-4f54-8850-2c5373b7eac9"}),
Expect(md).To(HaveKeyWithValue("musicbrainz_albumtype", []string{"album"})) HaveKeyWithValue("musicbrainz_albumartistid", []string{"89ad4ac3-39f7-470e-963a-56509c546377"}),
Expect(md).To(HaveKeyWithValue("musicbrainz_albumcomment", []string{"MP3"})) HaveKeyWithValue("musicbrainz_albumtype", []string{"album"}),
HaveKeyWithValue("musicbrainz_albumcomment", []string{"MP3"}),
))
}) })
It("detects embedded cover art correctly", func() { It("detects embedded cover art correctly", func() {
@ -244,14 +246,16 @@ Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗
ALBUMARTISTSORT : Shiina, Ringo ALBUMARTISTSORT : Shiina, Ringo
` `
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md).To(HaveKeyWithValue("title", []string{"ドツペルゲンガー"})) Expect(md).To(SatisfyAll(
Expect(md).To(HaveKeyWithValue("album", []string{"加爾基 精液 栗ノ花"})) HaveKeyWithValue("title", []string{"ドツペルゲンガー"}),
Expect(md).To(HaveKeyWithValue("artist", []string{"椎名林檎"})) HaveKeyWithValue("album", []string{"加爾基 精液 栗ノ花"}),
Expect(md).To(HaveKeyWithValue("album_artist", []string{"椎名林檎"})) HaveKeyWithValue("artist", []string{"椎名林檎"}),
Expect(md).To(HaveKeyWithValue("title-sort", []string{"Dopperugengā"})) HaveKeyWithValue("album_artist", []string{"椎名林檎"}),
Expect(md).To(HaveKeyWithValue("albumsort", []string{"Kalk Samen Kuri No Hana"})) HaveKeyWithValue("title-sort", []string{"Dopperugengā"}),
Expect(md).To(HaveKeyWithValue("artist_sort", []string{"Shiina, Ringo"})) HaveKeyWithValue("albumsort", []string{"Kalk Samen Kuri No Hana"}),
Expect(md).To(HaveKeyWithValue("albumartistsort", []string{"Shiina, Ringo"})) HaveKeyWithValue("artist_sort", []string{"Shiina, Ringo"}),
HaveKeyWithValue("albumartistsort", []string{"Shiina, Ringo"}),
))
}) })
It("ignores cover comment", func() { It("ignores cover comment", func() {
@ -310,11 +314,12 @@ Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hu
replaygain: track gain - -1.480000, track peak - 0.000011, album gain - 3.215180, album peak - 0.000021, replaygain: track gain - -1.480000, track peak - 0.000011, album gain - 3.215180, album peak - 0.000021,
` `
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md).To(HaveKeyWithValue("replaygain_track_gain", []string{"-1.48 dB"})) Expect(md).To(SatisfyAll(
Expect(md).To(HaveKeyWithValue("replaygain_track_peak", []string{"0.4512"})) HaveKeyWithValue("replaygain_track_gain", []string{"-1.48 dB"}),
Expect(md).To(HaveKeyWithValue("replaygain_album_gain", []string{"+3.21518 dB"})) HaveKeyWithValue("replaygain_track_peak", []string{"0.4512"}),
Expect(md).To(HaveKeyWithValue("replaygain_album_peak", []string{"0.9125"})) HaveKeyWithValue("replaygain_album_gain", []string{"+3.21518 dB"}),
HaveKeyWithValue("replaygain_album_peak", []string{"0.9125"}),
))
}) })
It("parses lyrics with language code", func() { It("parses lyrics with language code", func() {
@ -327,12 +332,14 @@ Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hu
: [00:02.50]unspecified : [00:02.50]unspecified
` `
md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
Expect(md).To(HaveKeyWithValue("lyrics-eng", []string{ Expect(md).To(SatisfyAll(
"[00:00.00]This is\n[00:02.50]English", HaveKeyWithValue("lyrics-eng", []string{
})) "[00:00.00]This is\n[00:02.50]English",
Expect(md).To(HaveKeyWithValue("lyrics-xxx", []string{ }),
"[00:00.00]This is\n[00:02.50]unspecified", HaveKeyWithValue("lyrics-xxx", []string{
})) "[00:00.00]This is\n[00:02.50]unspecified",
}),
))
}) })
It("parses normal LYRICS tag", func() { It("parses normal LYRICS tag", func() {

View File

@ -233,7 +233,7 @@ func h501(r chi.Router, paths ...string) {
for _, path := range paths { for _, path := range paths {
handle := func(w http.ResponseWriter, r *http.Request) { handle := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "no-cache") w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(501) w.WriteHeader(http.StatusNotImplemented)
_, _ = w.Write([]byte("This endpoint is not implemented, but may be in future releases")) _, _ = w.Write([]byte("This endpoint is not implemented, but may be in future releases"))
} }
addHandler(r, path, handle) addHandler(r, path, handle)
@ -244,7 +244,7 @@ func h501(r chi.Router, paths ...string) {
func h410(r chi.Router, paths ...string) { func h410(r chi.Router, paths ...string) {
for _, path := range paths { for _, path := range paths {
handle := func(w http.ResponseWriter, r *http.Request) { handle := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(410) w.WriteHeader(http.StatusGone)
_, _ = w.Write([]byte("This endpoint will not be implemented")) _, _ = w.Write([]byte("This endpoint will not be implemented"))
} }
addHandler(r, path, handle) addHandler(r, path, handle)
@ -276,7 +276,7 @@ func mapToSubsonicError(err error) subError {
func sendError(w http.ResponseWriter, r *http.Request, err error) { func sendError(w http.ResponseWriter, r *http.Request, err error) {
subErr := mapToSubsonicError(err) subErr := mapToSubsonicError(err)
response := newResponse() response := newResponse()
response.Status = "failed" response.Status = responses.StatusFailed
response.Error = &responses.Error{Code: int32(subErr.code), Message: subErr.Error()} response.Error = &responses.Error{Code: int32(subErr.code), Message: subErr.Error()}
sendResponse(w, r, response) sendResponse(w, r, response)
@ -305,11 +305,10 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
// This should never happen, but if it does, we need to know // This should never happen, but if it does, we need to know
if err != nil { if err != nil {
log.Error(r.Context(), "Error marshalling response", "format", f, err) log.Error(r.Context(), "Error marshalling response", "format", f, err)
w.WriteHeader(http.StatusInternalServerError) sendError(w, r, err)
_, _ = w.Write([]byte("Internal Server Error: " + err.Error()))
return return
} }
if payload.Status == "ok" { if payload.Status == responses.StatusOK {
if log.IsGreaterOrEqualTo(log.LevelTrace) { if log.IsGreaterOrEqualTo(log.LevelTrace) {
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response)) log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response))
} else { } else {

View File

@ -24,12 +24,12 @@ var _ = Describe("sendResponse", func() {
w = httptest.NewRecorder() w = httptest.NewRecorder()
r = httptest.NewRequest("GET", "/somepath", nil) r = httptest.NewRequest("GET", "/somepath", nil)
payload = &responses.Subsonic{ payload = &responses.Subsonic{
Status: "ok", Status: responses.StatusOK,
Version: "1.16.1", Version: "1.16.1",
} }
}) })
Context("when format is JSON", func() { When("format is JSON", func() {
It("should set Content-Type to application/json and return the correct body", func() { It("should set Content-Type to application/json and return the correct body", func() {
q := r.URL.Query() q := r.URL.Query()
q.Add("f", "json") q.Add("f", "json")
@ -48,7 +48,7 @@ var _ = Describe("sendResponse", func() {
}) })
}) })
Context("when format is JSONP", func() { When("format is JSONP", func() {
It("should set Content-Type to application/javascript and return the correct callback body", func() { It("should set Content-Type to application/javascript and return the correct callback body", func() {
q := r.URL.Query() q := r.URL.Query()
q.Add("f", "jsonp") q.Add("f", "jsonp")
@ -60,8 +60,8 @@ var _ = Describe("sendResponse", func() {
Expect(w.Header().Get("Content-Type")).To(Equal("application/javascript")) Expect(w.Header().Get("Content-Type")).To(Equal("application/javascript"))
body := w.Body.String() body := w.Body.String()
Expect(body).To(SatisfyAll( Expect(body).To(SatisfyAll(
ContainSubstring("testCallback("), HavePrefix("testCallback("),
ContainSubstring(")"), HaveSuffix(")"),
)) ))
// Extract JSON from the JSONP response // Extract JSON from the JSONP response
@ -73,7 +73,7 @@ var _ = Describe("sendResponse", func() {
}) })
}) })
Context("when format is XML or unspecified", func() { When("format is XML or unspecified", func() {
It("should set Content-Type to application/xml and return the correct body", func() { It("should set Content-Type to application/xml and return the correct body", func() {
// No format specified, expecting XML by default // No format specified, expecting XML by default
sendResponse(w, r, payload) sendResponse(w, r, payload)
@ -87,19 +87,25 @@ var _ = Describe("sendResponse", func() {
}) })
}) })
Context("when an error occurs during marshalling", func() { When("an error occurs during marshalling", func() {
It("should return HTTP 500", func() { It("should return a fail response", func() {
payload.Song = &responses.Child{ payload.Song = &responses.Child{
// An +Inf value will cause an error when marshalling to JSON
ReplayGain: responses.ReplayGain{TrackGain: math.Inf(1)}, ReplayGain: responses.ReplayGain{TrackGain: math.Inf(1)},
} // This will cause an error when marshalling to JSON }
q := r.URL.Query() q := r.URL.Query()
q.Add("f", "json") q.Add("f", "json")
r.URL.RawQuery = q.Encode() r.URL.RawQuery = q.Encode()
sendResponse(w, r, payload) sendResponse(w, r, payload)
Expect(w.Code).To(Equal(http.StatusInternalServerError)) Expect(w.Code).To(Equal(http.StatusOK))
body := w.Body.String() var wrapper responses.JsonWrapper
Expect(body).To(ContainSubstring("Internal Server Error")) err := json.Unmarshal(w.Body.Bytes(), &wrapper)
Expect(err).NotTo(HaveOccurred())
Expect(wrapper.Subsonic.Status).To(Equal(responses.StatusFailed))
Expect(wrapper.Subsonic.Version).To(Equal(payload.Version))
Expect(wrapper.Subsonic.Error.Message).To(ContainSubstring("json: unsupported value: +Inf"))
}) })
}) })

View File

@ -19,7 +19,7 @@ import (
func newResponse() *responses.Subsonic { func newResponse() *responses.Subsonic {
return &responses.Subsonic{ return &responses.Subsonic{
Status: "ok", Status: responses.StatusOK,
Version: Version, Version: Version,
Type: consts.AppName, Type: consts.AppName,
ServerVersion: consts.Version, ServerVersion: consts.Version,

View File

@ -61,6 +61,11 @@ type Subsonic struct {
LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"` LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"`
} }
const (
StatusOK = "ok"
StatusFailed = "failed"
)
type JsonWrapper struct { type JsonWrapper struct {
Subsonic Subsonic `json:"subsonic-response"` Subsonic Subsonic `json:"subsonic-response"`
} }

View File

@ -20,7 +20,7 @@ var _ = Describe("Responses", func() {
var response *Subsonic var response *Subsonic
BeforeEach(func() { BeforeEach(func() {
response = &Subsonic{ response = &Subsonic{
Status: "ok", Status: StatusOK,
Version: "1.8.0", Version: "1.8.0",
Type: consts.AppName, Type: consts.AppName,
ServerVersion: "v0.0.0", ServerVersion: "v0.0.0",