diff --git a/api/stream.go b/api/stream.go new file mode 100644 index 000000000..dd0d2700c --- /dev/null +++ b/api/stream.go @@ -0,0 +1,44 @@ +package api + +import ( + "github.com/deluan/gosonic/utils" + "github.com/karlkfi/inject" + "github.com/deluan/gosonic/domain" + "github.com/deluan/gosonic/api/responses" + "github.com/astaxie/beego" + "io" + "os" +) + + +type StreamController struct { + BaseAPIController + repo domain.MediaFileRepository +} + +func (c *StreamController) Prepare() { + inject.ExtractAssignable(utils.Graph, &c.repo) +} + +// For realtime transcoding, see : http://stackoverflow.com/questions/19292113/not-buffered-http-responsewritter-in-golang +func (c *StreamController) Get() { + id := c.ValidateParameters("id", "id parameter required") + + mf, err := c.repo.Get(id) + if err != nil { + beego.Error("Error reading mediafile", id, "from the database", ":", err) + c.SendError(responses.ERROR_GENERIC, "Internal error") + } + beego.Debug("Streaming file", mf.Path) + + f, err := os.Open(mf.Path) + if err != nil { + beego.Warn("Error opening file", mf.Path, "-", err) + c.SendError(responses.ERROR_DATA_NOT_FOUND, "cover art not available") + } + + c.Ctx.Output.ContentType(mf.ContentType()) + io.Copy(c.Ctx.ResponseWriter, f) + + beego.Debug("Finished streaming of", mf.Path) +} diff --git a/api/stream_test.go b/api/stream_test.go new file mode 100644 index 000000000..c704fbccb --- /dev/null +++ b/api/stream_test.go @@ -0,0 +1,58 @@ +package api_test + +import ( + "testing" + + "github.com/deluan/gosonic/api/responses" + "github.com/deluan/gosonic/domain" + . "github.com/deluan/gosonic/tests" + "github.com/deluan/gosonic/tests/mocks" + "github.com/deluan/gosonic/utils" + . "github.com/smartystreets/goconvey/convey" + "net/http" + "net/http/httptest" + "github.com/astaxie/beego" + "fmt" +) + +func stream(params ...string) (*http.Request, *httptest.ResponseRecorder) { + url := AddParams("/rest/stream.view", params...) + r, _ := http.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + beego.BeeApp.Handlers.ServeHTTP(w, r) + beego.Debug("testing TestStream", fmt.Sprintf("\nUrl: %s\nStatus Code: [%d]\n%#v", r.URL, w.Code, w.HeaderMap)) + return r, w +} + +func TestStream(t *testing.T) { + Init(t, false) + + mockMediaFileRepo := mocks.CreateMockMediaFileRepo() + utils.DefineSingleton(new(domain.MediaFileRepository), func() domain.MediaFileRepository { + return mockMediaFileRepo + }) + + Convey("Subject: Stream Endpoint", t, func() { + Convey("Should fail if missing Id parameter", func() { + _, w := stream() + + So(w.Body, ShouldReceiveError, responses.ERROR_MISSING_PARAMETER) + }) + Convey("When id is not found", func() { + mockMediaFileRepo.SetData(`[]`, 1) + _, w := stream("id=NOT_FOUND") + + So(w.Body, ShouldReceiveError, responses.ERROR_DATA_NOT_FOUND) + }) + Convey("When id is found", func() { + mockMediaFileRepo.SetData(`[{"Id":"2","HasCoverArt":true,"Path":"tests/fixtures/01 Invisible (RED) Edit Version.mp3"}]`, 1) + _, w := stream("id=2") + + So(w.Body.Bytes(), ShouldMatchMD5, "258dd4f0e70ee5c8dee3cb33c966acec") + }) + Reset(func() { + mockMediaFileRepo.SetData("[]", 0) + mockMediaFileRepo.SetError(false) + }) + }) +} diff --git a/conf/router.go b/conf/router.go index 15738a770..e29f989e5 100644 --- a/conf/router.go +++ b/conf/router.go @@ -22,6 +22,7 @@ func mapEndpoints() { beego.NSRouter("/getIndexes.view", &api.GetIndexesController{}, "*:Get"), beego.NSRouter("/getMusicDirectory.view", &api.GetMusicDirectoryController{}, "*:Get"), beego.NSRouter("/getCoverArt.view", &api.GetCoverArtController{}, "*:Get"), + beego.NSRouter("/stream.view", &api.StreamController{}, "*:Get"), ) beego.AddNamespace(ns)