diff --git a/api/media_annotation.go b/api/media_annotation.go index 18f1591ed..30ce899ea 100644 --- a/api/media_annotation.go +++ b/api/media_annotation.go @@ -24,18 +24,27 @@ func (c *MediaAnnotationController) Scrobble() { time := c.ParamTime("time", time.Now()) submission := c.ParamBool("submission", false) + playerId := 1 // TODO Multiple players, based on playerName/username/clientIP(?) playerName := c.ParamString("c") username := c.ParamString("u") + skip, err := c.scrobbler.DetectSkipped(playerId, id, submission) + if err { + beego.Error("Error detecting skip:", err) + } + if skip { + beego.Info("Skipped previous song") + } + if submission { - mf, err := c.scrobbler.Register(id, time) + mf, err := c.scrobbler.Register(playerId, id, time) if err != nil { beego.Error("Error scrobbling:", err) c.SendError(responses.ERROR_GENERIC, "Internal error") } beego.Info(fmt.Sprintf(`Scrobbled (%s) "%s" at %v`, id, mf.Title, time)) } else { - mf, err := c.scrobbler.NowPlaying(id, username, playerName) + mf, err := c.scrobbler.NowPlaying(playerId, 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/engine/mock_nowplaying_repo.go b/engine/mock_nowplaying_repo.go index 024335024..695d3d5ff 100644 --- a/engine/mock_nowplaying_repo.go +++ b/engine/mock_nowplaying_repo.go @@ -31,6 +31,12 @@ func (m *MockNowPlaying) Set(id, username string, playerId int, playerName strin return nil } +func (m *MockNowPlaying) Clear(playerId int) (*NowPlayingInfo, error) { + r := m.info + m.info = NowPlayingInfo{} + return &r, nil +} + func (m *MockNowPlaying) Current() NowPlayingInfo { return m.info } diff --git a/engine/nowplaying.go b/engine/nowplaying.go index e875be61a..c0bf13525 100644 --- a/engine/nowplaying.go +++ b/engine/nowplaying.go @@ -14,5 +14,6 @@ type NowPlayingInfo struct { type NowPlayingRepository interface { Set(trackId, username string, playerId int, playerName string) error + Clear(playerId int) (*NowPlayingInfo, error) GetAll() (*[]NowPlayingInfo, error) } diff --git a/engine/scrobbler.go b/engine/scrobbler.go index 32c060b93..d0a69a55d 100644 --- a/engine/scrobbler.go +++ b/engine/scrobbler.go @@ -10,8 +10,9 @@ import ( ) type Scrobbler interface { - Register(trackId string, playDate time.Time) (*domain.MediaFile, error) - NowPlaying(trackId, username string, playerName string) (*domain.MediaFile, error) + Register(playerId int, trackId string, playDate time.Time) (*domain.MediaFile, error) + NowPlaying(playerId int, trackId, username string, playerName string) (*domain.MediaFile, error) + DetectSkipped(playerId int, trackId string, submission bool) (bool, error) } func NewScrobbler(itunes itunesbridge.ItunesControl, mr domain.MediaFileRepository, npr NowPlayingRepository) Scrobbler { @@ -24,7 +25,23 @@ type scrobbler struct { npRepo NowPlayingRepository } -func (s *scrobbler) Register(id string, playDate time.Time) (*domain.MediaFile, error) { +func (s *scrobbler) DetectSkipped(playerId int, trackId string, submission bool) (bool, error) { + np, err := s.npRepo.Clear(playerId) + if err != nil { + return false, err + } + + if np == nil { + return false, nil + } + + if (submission && np.TrackId != trackId) || (!submission) { + return true, s.itunes.MarkAsSkipped(np.TrackId, time.Now()) + } + return false, nil +} + +func (s *scrobbler) Register(playerId int, id string, playDate time.Time) (*domain.MediaFile, error) { mf, err := s.mfRepo.Get(id) if err != nil { return nil, err @@ -40,7 +57,7 @@ func (s *scrobbler) Register(id string, playDate time.Time) (*domain.MediaFile, return mf, nil } -func (s *scrobbler) NowPlaying(trackId, username string, playerName string) (*domain.MediaFile, error) { +func (s *scrobbler) NowPlaying(playerId int, trackId, username string, playerName string) (*domain.MediaFile, error) { mf, err := s.mfRepo.Get(trackId) if err != nil { return nil, err @@ -50,5 +67,5 @@ func (s *scrobbler) NowPlaying(trackId, username string, playerName string) (*do return nil, errors.New(fmt.Sprintf(`Id "%s" not found`, trackId)) } - return mf, s.npRepo.Set(trackId, username, 1, playerName) + return mf, s.npRepo.Set(trackId, username, playerId, playerName) } diff --git a/engine/scrobbler_test.go b/engine/scrobbler_test.go index 2a01765dc..6a95e43de 100644 --- a/engine/scrobbler_test.go +++ b/engine/scrobbler_test.go @@ -27,7 +27,7 @@ func TestScrobbler(t *testing.T) { Convey("When I scrobble an existing song", func() { now := time.Now() - mf, err := scrobbler.Register("2", now) + mf, err := scrobbler.Register(1, "2", now) Convey("Then I get the scrobbled song back", func() { So(err, ShouldBeNil) @@ -42,7 +42,7 @@ func TestScrobbler(t *testing.T) { }) Convey("When the ID is not in the DB", func() { - _, err := scrobbler.Register("3", time.Now()) + _, err := scrobbler.Register(1, "3", time.Now()) Convey("Then I receive an error", func() { So(err, ShouldNotBeNil) @@ -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", "deluan", "DSub") + mf, err := scrobbler.NowPlaying(1, "2", "deluan", "DSub") Convey("Then I get the song for that id back", func() { So(err, ShouldBeNil) @@ -79,13 +79,38 @@ func TestScrobbler(t *testing.T) { }) }) - + Convey("Given a DB with two songs", t, func() { + mfRepo.SetData(`[{"Id":"1","Title":"Femme Fatale"},{"Id":"2","Title":"Here She Comes Now"}]`, 2) + Convey("When I play one song", func() { + scrobbler.NowPlaying(1, "1", "deluan", "DSub") + Convey("And I start playing the other song without scrobbling the first one", func() { + skip, err := scrobbler.DetectSkipped(1, "2", false) + Convey("Then the first song should be marked as skipped", func() { + So(skip, ShouldBeTrue) + So(itCtrl.skipped, ShouldContainKey, "1") + So(err, ShouldBeNil) + }) + }) + Convey("And I scrobble it before starting to play the other song", func() { + skip, err := scrobbler.DetectSkipped(1, "1", true) + Convey("Then the first song should NOT marked as skipped", func() { + So(skip, ShouldBeFalse) + So(itCtrl.skipped, ShouldBeEmpty) + So(err, ShouldBeNil) + }) + }) + Reset(func() { + itCtrl.skipped = make(map[string]time.Time) + }) + }) + }) } type mockItunesControl struct { itunesbridge.ItunesControl - played map[string]time.Time - error bool + played map[string]time.Time + skipped map[string]time.Time + error bool } func (m *mockItunesControl) MarkAsPlayed(id string, playDate time.Time) error { @@ -98,3 +123,14 @@ func (m *mockItunesControl) MarkAsPlayed(id string, playDate time.Time) error { m.played[id] = playDate return nil } + +func (m *mockItunesControl) MarkAsSkipped(id string, skipDate time.Time) error { + if m.error { + return errors.New("ID not found") + } + if m.skipped == nil { + m.skipped = make(map[string]time.Time) + } + m.skipped[id] = skipDate + return nil +} diff --git a/itunesbridge/itunes.go b/itunesbridge/itunes.go index 57b26b51d..46838304d 100644 --- a/itunesbridge/itunes.go +++ b/itunesbridge/itunes.go @@ -7,6 +7,7 @@ import ( type ItunesControl interface { MarkAsPlayed(id string, playDate time.Time) error + MarkAsSkipped(id string, skipDate time.Time) error } func NewItunesControl() ItunesControl { @@ -26,6 +27,17 @@ func (c *itunesControl) MarkAsPlayed(id string, playDate time.Time) error { return script.Run() } +func (c *itunesControl) MarkAsSkipped(id string, skipDate time.Time) error { + script := Script{fmt.Sprintf( + `set theTrack to the first item of (every track whose database ID is equal to "%s")`, id), + `set c to (get skipped count of theTrack)`, + `tell theTrack`, + `set skipped count to c + 1`, + fmt.Sprintf(`set skipped date to date("%s")`, c.formatDateTime(skipDate)), + `end tell`} + return script.Run() +} + func (c *itunesControl) formatDateTime(d time.Time) string { return d.Format("Jan _2, 2006 3:04PM") } diff --git a/persistence/nowplaying_repository.go b/persistence/nowplaying_repository.go index def5a8ef2..655f690d4 100644 --- a/persistence/nowplaying_repository.go +++ b/persistence/nowplaying_repository.go @@ -35,6 +35,19 @@ func (r *nowPlayingRepository) Set(id, username string, playerId int, playerName return Db().SetEX(nowPlayingKeyName, int64(engine.NowPlayingExpire.Seconds()), []byte(h)) } +func (r *nowPlayingRepository) Clear(playerId int) (*engine.NowPlayingInfo, error) { + val, err := Db().GetSet(nowPlayingKeyName, nil) + if err != nil { + return nil, err + } + info := &engine.NowPlayingInfo{} + err = json.Unmarshal(val, info) + if err != nil { + return nil, nil + } + return info, nil +} + func (r *nowPlayingRepository) GetAll() (*[]engine.NowPlayingInfo, error) { val, err := Db().Get(nowPlayingKeyName) if err != nil { @@ -45,8 +58,10 @@ func (r *nowPlayingRepository) GetAll() (*[]engine.NowPlayingInfo, error) { } info := &engine.NowPlayingInfo{} err = json.Unmarshal(val, info) - - return &[]engine.NowPlayingInfo{*info}, err + if err != nil { + return &[]engine.NowPlayingInfo{}, nil + } + return &[]engine.NowPlayingInfo{*info}, nil } var _ engine.NowPlayingRepository = (*nowPlayingRepository)(nil)