diff --git a/api/album_list.go b/api/album_list.go index d443ad23e..311582564 100644 --- a/api/album_list.go +++ b/api/album_list.go @@ -88,3 +88,10 @@ func (c *AlbumListController) GetStarred() { c.SendResponse(response) } + +func (c *AlbumListController) GetNowPlaying() { + response := c.NewEmpty() + response.NowPlaying = &responses.NowPlaying{} + //response.NowPlaying.Entry = make([]responses.NowPlayingEntry, 1) + c.SendResponse(response) +} diff --git a/api/album_list_test.go b/api/album_list_test.go index e60cbfa28..d63045684 100644 --- a/api/album_list_test.go +++ b/api/album_list_test.go @@ -5,6 +5,7 @@ import ( "github.com/deluan/gosonic/api/responses" "github.com/deluan/gosonic/domain" + "github.com/deluan/gosonic/engine" "github.com/deluan/gosonic/persistence" . "github.com/deluan/gosonic/tests" "github.com/deluan/gosonic/utils" @@ -19,6 +20,11 @@ func TestGetAlbumList(t *testing.T) { return mockAlbumRepo }) + mockNowPlayingRepo := engine.CreateMockNowPlayingRepo() + utils.DefineSingleton(new(engine.NowPlayingRepository), func() engine.NowPlayingRepository { + return mockNowPlayingRepo + }) + Convey("Subject: GetAlbumList Endpoint", t, func() { mockAlbumRepo.SetData(`[ {"Id":"A","Name":"Vagarosa","ArtistId":"2"}, diff --git a/api/media_annotation.go b/api/media_annotation.go index 5e9deec2f..886207be7 100644 --- a/api/media_annotation.go +++ b/api/media_annotation.go @@ -23,6 +23,10 @@ func (c *MediaAnnotationController) Scrobble() { id := c.RequiredParamString("id", "Required id parameter is missing") time := c.ParamTime("time", time.Now()) submission := c.ParamBool("submission", false) + + playerName := c.Data["c"].(string) + username := c.Data["u"].(string) + if submission { mf, err := c.scrobbler.Register(id, time) if err != nil { @@ -31,7 +35,7 @@ func (c *MediaAnnotationController) Scrobble() { } beego.Info(fmt.Sprintf(`Scrobbled (%s) "%s" at %v`, id, mf.Title, time)) } else { - mf, err := c.scrobbler.NowPlaying(id) + mf, err := c.scrobbler.NowPlaying(id, username, playerName) if err != nil { beego.Error("Error setting", id, "as current song:", err) c.SendError(responses.ERROR_GENERIC, "Internal error") diff --git a/api/responses/responses.go b/api/responses/responses.go index 41a506e80..26f423879 100644 --- a/api/responses/responses.go +++ b/api/responses/responses.go @@ -18,8 +18,9 @@ type Subsonic struct { AlbumList *AlbumList `xml:"albumList,omitempty" json:"albumList,omitempty"` Playlists *Playlists `xml:"playlists,omitempty" json:"playlists,omitempty"` Playlist *PlaylistWithSongs `xml:"playlist,omitempty" json:"playlist,omitempty"` - SearchResult2 *SearchResult2 `xml:"searchResult2,omitempty" json:"searchResult2,omitempty"` - Starred *Starred `xml:"starred,omitempty" json:"starred,omitempty"` + SearchResult2 *SearchResult2 `xml:"searchResult2,omitempty" json:"searchResult2,omitempty"` + Starred *Starred `xml:"starred,omitempty" json:"starred,omitempty"` + NowPlaying *NowPlaying `xml:"nowPlaying,omitempty" json:"nowPlaying,omitempty"` } type JsonWrapper struct { @@ -138,6 +139,18 @@ type Starred struct { Song []Child `xml:"song" json:"song,omitempty"` } +type NowPlayingEntry struct { + Child + UserName string `xml:"username" json:"username,omitempty"` + MinutesAgo int `xml:"minutesAgo" json:"minutesAgo,omitempty"` + PlayerId int `xml:"playerId" json:"playerId,omitempty"` + PlayerName string `xml:"playerName" json:"playerName,omitempty"` +} + +type NowPlaying struct { + Entry []NowPlayingEntry `xml:"entry" json:"entry,omitempty"` +} + type User struct { Username string `xml:"username,attr" json:"username"` Email string `xml:"email,attr,omitempty" json:"email,omitempty"` diff --git a/api/validation.go b/api/validation.go index 0548628a3..b314c9ecb 100644 --- a/api/validation.go +++ b/api/validation.go @@ -32,6 +32,7 @@ func checkParameters(c BaseAPIController) { logWarn(c, fmt.Sprintf(`Missing required parameter "%s"`, p)) abortRequest(c, responses.ERROR_MISSING_PARAMETER) } + c.Data[p] = c.GetString(p) } } diff --git a/conf/router.go b/conf/router.go index 922c97f3f..4fa59a19e 100644 --- a/conf/router.go +++ b/conf/router.go @@ -33,6 +33,7 @@ func mapEndpoints() { beego.NSRouter("/getAlbumList.view", &api.AlbumListController{}, "*:GetAlbumList"), beego.NSRouter("/getStarred.view", &api.AlbumListController{}, "*:GetStarred"), + beego.NSRouter("/getNowPlaying.view", &api.AlbumListController{}, "*:GetNowPlaying"), beego.NSRouter("/getPlaylists.view", &api.PlaylistsController{}, "*:GetAll"), beego.NSRouter("/getPlaylist.view", &api.PlaylistsController{}, "*:Get"), @@ -54,6 +55,7 @@ func mapFilters() { var ValidateRequest = func(ctx *context.Context) { c := api.BaseAPIController{} c.Ctx = ctx + c.Data = make(map[interface{}]interface{}) api.Validate(c) } diff --git a/engine/common.go b/engine/common.go index 2232915a7..49ba37f9e 100644 --- a/engine/common.go +++ b/engine/common.go @@ -24,6 +24,11 @@ type Entry struct { Suffix string BitRate int ContentType string + + UserName string + MinutesAgo int + PlayerId int + PlayerName string } type Entries []Entry diff --git a/engine/list_generator.go b/engine/list_generator.go index 51a4b1b8a..f96d9e93d 100644 --- a/engine/list_generator.go +++ b/engine/list_generator.go @@ -2,9 +2,12 @@ package engine import ( "math/rand" + "time" + "github.com/astaxie/beego" "github.com/deluan/gosonic/domain" "github.com/deluan/gosonic/utils" + "github.com/syndtr/goleveldb/leveldb/errors" ) // TODO Use Entries instead of Albums @@ -15,14 +18,17 @@ type ListGenerator interface { GetHighest(offset int, size int) (*domain.Albums, error) GetRandom(offset int, size int) (*domain.Albums, error) GetStarred() (*Entries, error) + GetNowPlaying() (*Entries, error) } -func NewListGenerator(alr domain.AlbumRepository) ListGenerator { - return listGenerator{alr} +func NewListGenerator(alr domain.AlbumRepository, mfr domain.MediaFileRepository, npr NowPlayingRepository) ListGenerator { + return listGenerator{alr, mfr, npr} } type listGenerator struct { - albumRepo domain.AlbumRepository + albumRepo domain.AlbumRepository + mfRepository domain.MediaFileRepository + npRepo NowPlayingRepository } func (g listGenerator) query(qo domain.QueryOptions, offset int, size int) (*domain.Albums, error) { @@ -84,3 +90,24 @@ func (g listGenerator) GetStarred() (*Entries, error) { return &entries, nil } + +func (g listGenerator) GetNowPlaying() (*Entries, error) { + npInfo, err := g.npRepo.GetAll() + if err != nil { + return nil, err + } + entries := make(Entries, len(*npInfo)) + for i, np := range *npInfo { + mf, err := g.mfRepository.Get(np.TrackId) + if err != nil { + return nil, err + } + entries[i] = FromMediaFile(mf) + entries[i].UserName = beego.AppConfig.String("user") + entries[i].MinutesAgo = int(time.Now().Sub(np.Start).Minutes()) + entries[i].PlayerId = np.PlayerId + entries[i].PlayerName = np.PlayerName + + } + return &entries, errors.New("Not implemented") +} diff --git a/engine/mock_nowplaying_repo.go b/engine/mock_nowplaying_repo.go index ef1ce5b8b..024335024 100644 --- a/engine/mock_nowplaying_repo.go +++ b/engine/mock_nowplaying_repo.go @@ -11,24 +11,26 @@ func CreateMockNowPlayingRepo() *MockNowPlaying { type MockNowPlaying struct { NowPlayingRepository - id string - start time.Time - err bool + info NowPlayingInfo + err bool } func (m *MockNowPlaying) SetError(err bool) { m.err = err } -func (m *MockNowPlaying) Set(id string) error { +func (m *MockNowPlaying) Set(id, username string, playerId int, playerName string) error { if m.err { return errors.New("Error!") } - m.id = id - m.start = time.Now() + m.info.TrackId = id + m.info.Username = username + m.info.Start = time.Now() + m.info.PlayerId = playerId + m.info.PlayerName = playerName return nil } -func (m *MockNowPlaying) Current() (string, time.Time) { - return m.id, m.start +func (m *MockNowPlaying) Current() NowPlayingInfo { + return m.info } diff --git a/engine/nowplaying.go b/engine/nowplaying.go index 3c624b674..28dfaedf0 100644 --- a/engine/nowplaying.go +++ b/engine/nowplaying.go @@ -5,10 +5,14 @@ import "time" const NowPlayingExpire = time.Duration(30) * time.Minute type NowPlayingInfo struct { - TrackId string - Start time.Time + TrackId string + Start time.Time + Username string + PlayerId int + PlayerName string } type NowPlayingRepository interface { - Set(trackId string) error + Set(trackId, username string, playerId int, playerName string) error + GetAll() (*[]NowPlayingInfo, error) } diff --git a/engine/scrobbler.go b/engine/scrobbler.go index 5474a2062..32c060b93 100644 --- a/engine/scrobbler.go +++ b/engine/scrobbler.go @@ -10,8 +10,8 @@ import ( ) type Scrobbler interface { - Register(id string, playDate time.Time) (*domain.MediaFile, error) - NowPlaying(id string) (*domain.MediaFile, error) + Register(trackId string, playDate time.Time) (*domain.MediaFile, error) + NowPlaying(trackId, username string, playerName string) (*domain.MediaFile, error) } func NewScrobbler(itunes itunesbridge.ItunesControl, mr domain.MediaFileRepository, npr NowPlayingRepository) Scrobbler { @@ -40,15 +40,15 @@ func (s *scrobbler) Register(id string, playDate time.Time) (*domain.MediaFile, return mf, nil } -func (s *scrobbler) NowPlaying(id string) (*domain.MediaFile, error) { - mf, err := s.mfRepo.Get(id) +func (s *scrobbler) NowPlaying(trackId, username string, playerName string) (*domain.MediaFile, error) { + mf, err := s.mfRepo.Get(trackId) if err != nil { return nil, err } if mf == nil { - return nil, errors.New(fmt.Sprintf(`Id "%s" not found`, id)) + return nil, errors.New(fmt.Sprintf(`Id "%s" not found`, trackId)) } - return mf, s.npRepo.Set(id) + return mf, s.npRepo.Set(trackId, username, 1, playerName) } diff --git a/engine/scrobbler_test.go b/engine/scrobbler_test.go index ec4dd74e0..2a01765dc 100644 --- a/engine/scrobbler_test.go +++ b/engine/scrobbler_test.go @@ -54,7 +54,7 @@ func TestScrobbler(t *testing.T) { }) Convey("When I inform the song that is now playing", func() { - mf, err := scrobbler.NowPlaying("2") + mf, err := scrobbler.NowPlaying("2", "deluan", "DSub") Convey("Then I get the song for that id back", func() { So(err, ShouldBeNil) @@ -62,9 +62,11 @@ func TestScrobbler(t *testing.T) { }) Convey("And it saves the song as the one current playing", func() { - id, start := npRepo.Current() - So(id, ShouldEqual, "2") - So(start, ShouldHappenBefore, time.Now()) + info := npRepo.Current() + So(info.TrackId, ShouldEqual, "2") + So(info.Start, ShouldHappenBefore, time.Now()) + So(info.Username, ShouldEqual, "deluan") + So(info.PlayerName, ShouldEqual, "DSub") }) Convey("And iTunes is not notified", func() { diff --git a/persistence/nowplaying_repository.go b/persistence/nowplaying_repository.go index 83d336216..40330f168 100644 --- a/persistence/nowplaying_repository.go +++ b/persistence/nowplaying_repository.go @@ -22,11 +22,11 @@ func NewNowPlayingRepository() engine.NowPlayingRepository { return r } -func (r *nowPlayingRepository) Set(id string) error { +func (r *nowPlayingRepository) Set(id, username string, playerId int, playerName string) error { if id == "" { return errors.New("Id is required") } - m := &engine.NowPlayingInfo{TrackId: id, Start: time.Now()} + m := &engine.NowPlayingInfo{TrackId: id, Username: username, Start: time.Now(), PlayerId: playerId, PlayerName: playerName} h, err := json.Marshal(m) if err != nil { @@ -35,4 +35,15 @@ func (r *nowPlayingRepository) Set(id string) error { return Db().SetEX(nowPlayingKeyName, int64(engine.NowPlayingExpire.Seconds()), []byte(h)) } +func (r *nowPlayingRepository) GetAll() (*[]engine.NowPlayingInfo, error) { + val, err := Db().Get(nowPlayingKeyName) + if err != nil { + return nil, err + } + info := &engine.NowPlayingInfo{} + err = json.Unmarshal(val, info) + + return &[]engine.NowPlayingInfo{*info}, err +} + var _ engine.NowPlayingRepository = (*nowPlayingRepository)(nil)