diff --git a/api/get_cover_art.go b/api/get_cover_art.go new file mode 100644 index 000000000..590cb380b --- /dev/null +++ b/api/get_cover_art.go @@ -0,0 +1,71 @@ +package api + +import ( + "github.com/deluan/gosonic/domain" + "github.com/karlkfi/inject" + "github.com/deluan/gosonic/utils" + "github.com/astaxie/beego" + "github.com/deluan/gosonic/api/responses" + "io/ioutil" +"github.com/dhowden/tag" + "os" +) + +type GetCoverArtController struct { + BaseAPIController + repo domain.MediaFileRepository +} + +func (c *GetCoverArtController) Prepare() { + inject.ExtractAssignable(utils.Graph, &c.repo) +} + +func (c *GetCoverArtController) Get() { + id := c.Input().Get("id") + if id == "" { + c.SendError(responses.ERROR_MISSING_PARAMETER, "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") + } + + var img []byte + + if (mf.HasCoverArt) { + img, err = readFromTag(mf.Path) + beego.Debug("Serving cover art from", mf.Path) + } else { + img, err = ioutil.ReadFile("static/default_cover.jpg") + beego.Debug("Serving default cover art") + } + + if err != nil { + beego.Error("Could not retrieve cover art", id, ":", err) + c.SendError(responses.ERROR_DATA_NOT_FOUND, "cover art not available") + } + + + c.Ctx.Output.ContentType("image/jpg") + c.Ctx.Output.Body(img) +} + +func readFromTag(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + beego.Warn("Error opening file", path, "-", err) + return nil, err + } + defer f.Close() + + m, err := tag.ReadFrom(f) + if err != nil { + beego.Warn("Error reading tag from file", path, "-", err) + return nil, err + } + + return m.Picture().Data, nil +} + diff --git a/api/get_cover_art_test.go b/api/get_cover_art_test.go new file mode 100644 index 000000000..6c268addd --- /dev/null +++ b/api/get_cover_art_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 getCoverArt(params ...string) (*http.Request, *httptest.ResponseRecorder) { + url := AddParams("/rest/getCoverArt.view", params...) + r, _ := http.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + beego.BeeApp.Handlers.ServeHTTP(w, r) + beego.Debug("testing TestGetCoverArtDirectory", fmt.Sprintf("\nUrl: %s\nStatus Code: [%d]\n%#v", r.URL, w.Code, w.HeaderMap)) + return r, w +} + +func TestGetCoverArt(t *testing.T) { + Init(t, false) + + mockMediaFileRepo := mocks.CreateMockMediaFileRepo() + utils.DefineSingleton(new(domain.MediaFileRepository), func() domain.MediaFileRepository { + return mockMediaFileRepo + }) + + Convey("Subject: GetCoverArt Endpoint", t, func() { + Convey("Should fail if missing Id parameter", func() { + _, w := getCoverArt() + + So(w.Body, ShouldReceiveError, responses.ERROR_MISSING_PARAMETER) + }) + Convey("When id is not found", func() { + mockMediaFileRepo.SetData(`[]`, 1) + _, w := getCoverArt("id=NOT_FOUND") + + So(w.Body.Bytes(), ShouldMatchMD5, "963552b04e87a5a55e993f98a0fbdf82") + }) + Convey("When id is found", func() { + mockMediaFileRepo.SetData(`[{"Id":"2","HasCoverArt":true,"Path":"tests/fixtures/01 Invisible (RED) Edit Version.mp3"}]`, 1) + _, w := getCoverArt("id=2") + + So(w.Body.Bytes(), ShouldMatchMD5, "e859a71cd1b1aaeb1ad437d85b306668") + }) + Reset(func() { + mockMediaFileRepo.SetData("[]", 0) + mockMediaFileRepo.SetError(false) + }) + }) +} diff --git a/conf/router.go b/conf/router.go index 468a63dd9..15738a770 100644 --- a/conf/router.go +++ b/conf/router.go @@ -21,6 +21,7 @@ func mapEndpoints() { beego.NSRouter("/getMusicFolders.view", &api.GetMusicFoldersController{}, "*:Get"), beego.NSRouter("/getIndexes.view", &api.GetIndexesController{}, "*:Get"), beego.NSRouter("/getMusicDirectory.view", &api.GetMusicDirectoryController{}, "*:Get"), + beego.NSRouter("/getCoverArt.view", &api.GetCoverArtController{}, "*:Get"), ) beego.AddNamespace(ns) diff --git a/domain/mediafile.go b/domain/mediafile.go index a41472f9b..4aed54416 100644 --- a/domain/mediafile.go +++ b/domain/mediafile.go @@ -30,5 +30,6 @@ type MediaFile struct { type MediaFileRepository interface { BaseRepository Put(m *MediaFile) error + Get(id string) (*MediaFile, error) FindByAlbum(albumId string) ([]MediaFile, error) } diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index 228deb80b..0a2747fd6 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -19,6 +19,11 @@ func (r *mediaFileRepository) Put(m *domain.MediaFile) error { return r.saveOrUpdate(m.Id, m) } +func (r *mediaFileRepository) Get(id string) (*domain.MediaFile, error) { + m, err := r.readEntity(id) + return m.(*domain.MediaFile), err +} + func (r *mediaFileRepository) FindByAlbum(albumId string) ([]domain.MediaFile, error) { var mfs = make([]domain.MediaFile, 0) err := r.loadChildren("album", albumId, &mfs, "", false) @@ -35,7 +40,7 @@ func (a byTrackNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byTrackNumber) Less(i, j int) bool { - return (a[i].DiscNumber*1000 + a[i].TrackNumber) < (a[j].DiscNumber*1000 + a[j].TrackNumber) + return (a[i].DiscNumber * 1000 + a[i].TrackNumber) < (a[j].DiscNumber * 1000 + a[j].TrackNumber) } var _ domain.MediaFileRepository = (*mediaFileRepository)(nil) \ No newline at end of file diff --git a/static/default_cover.jpg b/static/default_cover.jpg new file mode 100644 index 000000000..647bd4372 Binary files /dev/null and b/static/default_cover.jpg differ diff --git a/tests/fixtures/01 Invisible (RED) Edit Version.mp3 b/tests/fixtures/01 Invisible (RED) Edit Version.mp3 new file mode 100644 index 000000000..8abd358c0 Binary files /dev/null and b/tests/fixtures/01 Invisible (RED) Edit Version.mp3 differ diff --git a/tests/matchers.go b/tests/matchers.go index 1793335b3..1d142588a 100644 --- a/tests/matchers.go +++ b/tests/matchers.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/deluan/gosonic/api/responses" . "github.com/smartystreets/goconvey/convey" + "crypto/md5" ) func ShouldMatchXML(actual interface{}, expected ...interface{}) string { @@ -43,6 +44,11 @@ func ShouldReceiveError(actual interface{}, expected ...interface{}) string { return ShouldEqual(v.Error.Code, expected[0].(int)) } +func ShouldMatchMD5(actual interface{}, expected ...interface{}) string { + a := fmt.Sprintf("%x", md5.Sum(actual.([]byte))) + return ShouldEqual(a, expected[0].(string)) +} + func UnindentJSON(j []byte) string { var m = make(map[string]interface{}) json.Unmarshal(j, &m) diff --git a/tests/mocks/mock_mediafile_repo.go b/tests/mocks/mock_mediafile_repo.go index 15c5906d0..f0bf8b651 100644 --- a/tests/mocks/mock_mediafile_repo.go +++ b/tests/mocks/mock_mediafile_repo.go @@ -45,7 +45,11 @@ func (m *MockMediaFile) Get(id string) (*domain.MediaFile, error) { if m.err { return nil, errors.New("Error!") } - return m.data[id], nil + mf := m.data[id] + if mf == nil { + mf = &domain.MediaFile{} + } + return mf, nil } func (m *MockMediaFile) FindByAlbum(artistId string) ([]domain.MediaFile, error) {