diff --git a/engine/list_generator.go b/engine/list_generator.go index f9eec81f2..1123598ed 100644 --- a/engine/list_generator.go +++ b/engine/list_generator.go @@ -22,7 +22,7 @@ type ListGenerator interface { GetRandomSongs(size int) (Entries, error) } -func NewListGenerator(arr model.ArtistRepository, alr model.AlbumRepository, mfr model.MediaFileRepository, npr model.NowPlayingRepository) ListGenerator { +func NewListGenerator(arr model.ArtistRepository, alr model.AlbumRepository, mfr model.MediaFileRepository, npr NowPlayingRepository) ListGenerator { return &listGenerator{arr, alr, mfr, npr} } @@ -30,7 +30,7 @@ type listGenerator struct { artistRepo model.ArtistRepository albumRepo model.AlbumRepository mfRepository model.MediaFileRepository - npRepo model.NowPlayingRepository + npRepo NowPlayingRepository } func (g *listGenerator) query(qo model.QueryOptions, offset int, size int) (Entries, error) { diff --git a/engine/mock_nowplaying_repo.go b/engine/mock_nowplaying_repo.go index 0fd8b6f6d..718ee55c4 100644 --- a/engine/mock_nowplaying_repo.go +++ b/engine/mock_nowplaying_repo.go @@ -3,8 +3,6 @@ package engine import ( "errors" "time" - - "github.com/cloudsonic/sonic-server/model" ) func CreateMockNowPlayingRepo() *MockNowPlaying { @@ -12,8 +10,8 @@ func CreateMockNowPlayingRepo() *MockNowPlaying { } type MockNowPlaying struct { - model.NowPlayingRepository - data []model.NowPlayingInfo + NowPlayingRepository + data []NowPlayingInfo t time.Time err bool } @@ -22,12 +20,12 @@ func (m *MockNowPlaying) SetError(err bool) { m.err = err } -func (m *MockNowPlaying) Enqueue(info *model.NowPlayingInfo) error { +func (m *MockNowPlaying) Enqueue(info *NowPlayingInfo) error { if m.err { return errors.New("Error!") } - m.data = append(m.data, model.NowPlayingInfo{}) + m.data = append(m.data, NowPlayingInfo{}) copy(m.data[1:], m.data[0:]) m.data[0] = *info @@ -39,7 +37,7 @@ func (m *MockNowPlaying) Enqueue(info *model.NowPlayingInfo) error { return nil } -func (m *MockNowPlaying) Dequeue(playerId int) (*model.NowPlayingInfo, error) { +func (m *MockNowPlaying) Dequeue(playerId int) (*NowPlayingInfo, error) { if len(m.data) == 0 { return nil, nil } @@ -54,15 +52,15 @@ func (m *MockNowPlaying) Count(playerId int) (int64, error) { return int64(len(m.data)), nil } -func (m *MockNowPlaying) GetAll() ([]*model.NowPlayingInfo, error) { +func (m *MockNowPlaying) GetAll() ([]*NowPlayingInfo, error) { np, err := m.Head(1) if np == nil || err != nil { return nil, err } - return []*model.NowPlayingInfo{np}, err + return []*NowPlayingInfo{np}, err } -func (m *MockNowPlaying) Head(playerId int) (*model.NowPlayingInfo, error) { +func (m *MockNowPlaying) Head(playerId int) (*NowPlayingInfo, error) { if len(m.data) == 0 { return nil, nil } @@ -70,7 +68,7 @@ func (m *MockNowPlaying) Head(playerId int) (*model.NowPlayingInfo, error) { return &info, nil } -func (m *MockNowPlaying) Tail(playerId int) (*model.NowPlayingInfo, error) { +func (m *MockNowPlaying) Tail(playerId int) (*NowPlayingInfo, error) { if len(m.data) == 0 { return nil, nil } @@ -79,7 +77,7 @@ func (m *MockNowPlaying) Tail(playerId int) (*model.NowPlayingInfo, error) { } func (m *MockNowPlaying) ClearAll() { - m.data = make([]model.NowPlayingInfo, 0) + m.data = make([]NowPlayingInfo, 0) m.err = false } diff --git a/engine/nowplaying.go b/engine/nowplaying.go new file mode 100644 index 000000000..147ddfba2 --- /dev/null +++ b/engine/nowplaying.go @@ -0,0 +1,102 @@ +package engine + +import ( + "container/list" + "sync" + "time" +) + +const NowPlayingExpire = 60 * time.Minute + +type NowPlayingInfo struct { + TrackID string + Start time.Time + Username string + PlayerId int + PlayerName string +} + +// This repo must have the semantics of a FIFO queue, for each playerId +type NowPlayingRepository interface { + // Insert at the head of the queue + Enqueue(*NowPlayingInfo) error + + // Removes and returns the element at the end of the queue + Dequeue(playerId int) (*NowPlayingInfo, error) + + // Returns the element at the head of the queue (last inserted one) + Head(playerId int) (*NowPlayingInfo, error) + + // Returns the element at the end of the queue (first inserted one) + Tail(playerId int) (*NowPlayingInfo, error) + + // Size of the queue for the playerId + Count(playerId int) (int64, error) + + // Returns all heads from all playerIds + GetAll() ([]*NowPlayingInfo, error) +} + +var playerMap = sync.Map{} + +type nowPlayingRepository struct{} + +func NewNowPlayingRepository() NowPlayingRepository { + r := &nowPlayingRepository{} + return r +} + +func (r *nowPlayingRepository) getList(id int) *list.List { + l, _ := playerMap.LoadOrStore(id, list.New()) + return l.(*list.List) +} + +func (r *nowPlayingRepository) Enqueue(info *NowPlayingInfo) error { + l := r.getList(info.PlayerId) + l.PushFront(info) + return nil +} + +func (r *nowPlayingRepository) Dequeue(playerId int) (*NowPlayingInfo, error) { + l := r.getList(playerId) + e := l.Back() + if e == nil { + return nil, nil + } + l.Remove(e) + return e.Value.(*NowPlayingInfo), nil +} + +func (r *nowPlayingRepository) Head(playerId int) (*NowPlayingInfo, error) { + l := r.getList(playerId) + e := l.Front() + if e == nil { + return nil, nil + } + return e.Value.(*NowPlayingInfo), nil +} + +func (r *nowPlayingRepository) Tail(playerId int) (*NowPlayingInfo, error) { + l := r.getList(playerId) + e := l.Back() + if e == nil { + return nil, nil + } + return e.Value.(*NowPlayingInfo), nil +} + +func (r *nowPlayingRepository) Count(playerId int) (int64, error) { + l := r.getList(playerId) + return int64(l.Len()), nil +} + +func (r *nowPlayingRepository) GetAll() ([]*NowPlayingInfo, error) { + var all []*NowPlayingInfo + playerMap.Range(func(playerId, l interface{}) bool { + ll := l.(*list.List) + e := ll.Front() + all = append(all, e.Value.(*NowPlayingInfo)) + return true + }) + return all, nil +} diff --git a/engine/nowplaying_repository_test.go b/engine/nowplaying_repository_test.go new file mode 100644 index 000000000..d7debd2b4 --- /dev/null +++ b/engine/nowplaying_repository_test.go @@ -0,0 +1,49 @@ +package engine + +import ( + "sync" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("NowPlayingRepository", func() { + var repo NowPlayingRepository + + BeforeEach(func() { + playerMap = sync.Map{} + repo = NewNowPlayingRepository() + }) + + It("enqueues and dequeues records", func() { + Expect(repo.Enqueue(&NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})).To(BeNil()) + Expect(repo.Enqueue(&NowPlayingInfo{PlayerId: 1, TrackID: "BBB"})).To(BeNil()) + + Expect(repo.Tail(1)).To(Equal(&NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})) + Expect(repo.Head(1)).To(Equal(&NowPlayingInfo{PlayerId: 1, TrackID: "BBB"})) + + Expect(repo.Count(1)).To(Equal(int64(2))) + + Expect(repo.Dequeue(1)).To(Equal(&NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})) + Expect(repo.Count(1)).To(Equal(int64(1))) + }) + + It("handles multiple players", func() { + Expect(repo.Enqueue(&NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})).To(BeNil()) + Expect(repo.Enqueue(&NowPlayingInfo{PlayerId: 1, TrackID: "BBB"})).To(BeNil()) + + Expect(repo.Enqueue(&NowPlayingInfo{PlayerId: 2, TrackID: "CCC"})).To(BeNil()) + Expect(repo.Enqueue(&NowPlayingInfo{PlayerId: 2, TrackID: "DDD"})).To(BeNil()) + + Expect(repo.GetAll()).To(ConsistOf([]*NowPlayingInfo{ + {PlayerId: 1, TrackID: "BBB"}, + {PlayerId: 2, TrackID: "DDD"}, + })) + + Expect(repo.Count(2)).To(Equal(int64(2))) + Expect(repo.Count(2)).To(Equal(int64(2))) + + Expect(repo.Tail(1)).To(Equal(&NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})) + Expect(repo.Head(2)).To(Equal(&NowPlayingInfo{PlayerId: 2, TrackID: "DDD"})) + }) +}) diff --git a/engine/scrobbler.go b/engine/scrobbler.go index 3bf61d803..7a237c185 100644 --- a/engine/scrobbler.go +++ b/engine/scrobbler.go @@ -21,14 +21,14 @@ type Scrobbler interface { NowPlaying(ctx context.Context, playerId int, playerName, trackId, username string) (*model.MediaFile, error) } -func NewScrobbler(itunes itunesbridge.ItunesControl, mr model.MediaFileRepository, npr model.NowPlayingRepository) Scrobbler { +func NewScrobbler(itunes itunesbridge.ItunesControl, mr model.MediaFileRepository, npr NowPlayingRepository) Scrobbler { return &scrobbler{itunes, mr, npr} } type scrobbler struct { itunes itunesbridge.ItunesControl mfRepo model.MediaFileRepository - npRepo model.NowPlayingRepository + npRepo NowPlayingRepository } func (s *scrobbler) detectSkipped(ctx context.Context, playerId int, trackId string) { @@ -96,6 +96,6 @@ func (s *scrobbler) NowPlaying(ctx context.Context, playerId int, playerName, tr return nil, errors.New(fmt.Sprintf(`ID "%s" not found`, trackId)) } - info := &model.NowPlayingInfo{TrackID: trackId, Username: username, Start: time.Now(), PlayerId: playerId, PlayerName: playerName} + info := &NowPlayingInfo{TrackID: trackId, Username: username, Start: time.Now(), PlayerId: playerId, PlayerName: playerName} return mf, s.npRepo.Enqueue(info) } diff --git a/engine/wire_providers.go b/engine/wire_providers.go index 6d37f6b74..9efa60e5e 100644 --- a/engine/wire_providers.go +++ b/engine/wire_providers.go @@ -10,4 +10,5 @@ var Set = wire.NewSet( NewRatings, NewScrobbler, NewSearch, + NewNowPlayingRepository, ) diff --git a/model/nowplaying.go b/model/nowplaying.go deleted file mode 100644 index ceedfcf1b..000000000 --- a/model/nowplaying.go +++ /dev/null @@ -1,34 +0,0 @@ -package model - -import "time" - -const NowPlayingExpire = 60 * time.Minute - -type NowPlayingInfo struct { - TrackID string - Start time.Time - Username string - PlayerId int - PlayerName string -} - -// This repo must have the semantics of a FIFO queue, for each playerId -type NowPlayingRepository interface { - // Insert at the head of the queue - Enqueue(*NowPlayingInfo) error - - // Removes and returns the element at the end of the queue - Dequeue(playerId int) (*NowPlayingInfo, error) - - // Returns the element at the head of the queue (last inserted one) - Head(playerId int) (*NowPlayingInfo, error) - - // Returns the element at the end of the queue (first inserted one) - Tail(playerId int) (*NowPlayingInfo, error) - - // Size of the queue for the playerId - Count(playerId int) (int64, error) - - // Returns all heads from all playerIds - GetAll() ([]*NowPlayingInfo, error) -} diff --git a/persistence/nowplaying_repository.go b/persistence/nowplaying_repository.go deleted file mode 100644 index 1dfe3b10a..000000000 --- a/persistence/nowplaying_repository.go +++ /dev/null @@ -1,75 +0,0 @@ -package persistence - -import ( - "container/list" - "sync" - - "github.com/cloudsonic/sonic-server/model" -) - -var playerMap = sync.Map{} - -type nowPlayingRepository struct{} - -// TODO Make it persistent -func NewNowPlayingRepository() model.NowPlayingRepository { - r := &nowPlayingRepository{} - return r -} - -func (r *nowPlayingRepository) getList(id int) *list.List { - l, _ := playerMap.LoadOrStore(id, list.New()) - return l.(*list.List) -} - -func (r *nowPlayingRepository) Enqueue(info *model.NowPlayingInfo) error { - l := r.getList(info.PlayerId) - l.PushFront(info) - return nil -} - -func (r *nowPlayingRepository) Dequeue(playerId int) (*model.NowPlayingInfo, error) { - l := r.getList(playerId) - e := l.Back() - if e == nil { - return nil, nil - } - l.Remove(e) - return e.Value.(*model.NowPlayingInfo), nil -} - -func (r *nowPlayingRepository) Head(playerId int) (*model.NowPlayingInfo, error) { - l := r.getList(playerId) - e := l.Front() - if e == nil { - return nil, nil - } - return e.Value.(*model.NowPlayingInfo), nil -} - -func (r *nowPlayingRepository) Tail(playerId int) (*model.NowPlayingInfo, error) { - l := r.getList(playerId) - e := l.Back() - if e == nil { - return nil, nil - } - return e.Value.(*model.NowPlayingInfo), nil -} - -func (r *nowPlayingRepository) Count(playerId int) (int64, error) { - l := r.getList(playerId) - return int64(l.Len()), nil -} - -func (r *nowPlayingRepository) GetAll() ([]*model.NowPlayingInfo, error) { - var all []*model.NowPlayingInfo - playerMap.Range(func(playerId, l interface{}) bool { - ll := l.(*list.List) - e := ll.Front() - all = append(all, e.Value.(*model.NowPlayingInfo)) - return true - }) - return all, nil -} - -var _ model.NowPlayingRepository = (*nowPlayingRepository)(nil) diff --git a/persistence/nowplaying_repository_test.go b/persistence/nowplaying_repository_test.go deleted file mode 100644 index 415310906..000000000 --- a/persistence/nowplaying_repository_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package persistence - -import ( - "sync" - - "github.com/cloudsonic/sonic-server/model" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("NowPlayingRepository", func() { - var repo model.NowPlayingRepository - - BeforeEach(func() { - playerMap = sync.Map{} - repo = NewNowPlayingRepository() - }) - - It("enqueues and dequeues records", func() { - Expect(repo.Enqueue(&model.NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})).To(BeNil()) - Expect(repo.Enqueue(&model.NowPlayingInfo{PlayerId: 1, TrackID: "BBB"})).To(BeNil()) - - Expect(repo.Tail(1)).To(Equal(&model.NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})) - Expect(repo.Head(1)).To(Equal(&model.NowPlayingInfo{PlayerId: 1, TrackID: "BBB"})) - - Expect(repo.Count(1)).To(Equal(int64(2))) - - Expect(repo.Dequeue(1)).To(Equal(&model.NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})) - Expect(repo.Count(1)).To(Equal(int64(1))) - }) - - It("handles multiple players", func() { - Expect(repo.Enqueue(&model.NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})).To(BeNil()) - Expect(repo.Enqueue(&model.NowPlayingInfo{PlayerId: 1, TrackID: "BBB"})).To(BeNil()) - - Expect(repo.Enqueue(&model.NowPlayingInfo{PlayerId: 2, TrackID: "CCC"})).To(BeNil()) - Expect(repo.Enqueue(&model.NowPlayingInfo{PlayerId: 2, TrackID: "DDD"})).To(BeNil()) - - Expect(repo.GetAll()).To(ConsistOf([]*model.NowPlayingInfo{ - {PlayerId: 1, TrackID: "BBB"}, - {PlayerId: 2, TrackID: "DDD"}, - })) - - Expect(repo.Count(2)).To(Equal(int64(2))) - Expect(repo.Count(2)).To(Equal(int64(2))) - - Expect(repo.Tail(1)).To(Equal(&model.NowPlayingInfo{PlayerId: 1, TrackID: "AAA"})) - Expect(repo.Head(2)).To(Equal(&model.NowPlayingInfo{PlayerId: 2, TrackID: "DDD"})) - }) -}) diff --git a/persistence/wire_provider.go b/persistence/wire_provider.go index e782184d5..e680e7e7e 100644 --- a/persistence/wire_provider.go +++ b/persistence/wire_provider.go @@ -11,7 +11,6 @@ var Set = wire.NewSet( NewCheckSumRepository, NewPropertyRepository, NewPlaylistRepository, - NewNowPlayingRepository, NewMediaFolderRepository, NewGenreRepository, ) diff --git a/wire_gen.go b/wire_gen.go index cf601d319..582c9e4fa 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -42,7 +42,7 @@ func CreateSubsonicAPIRouter() *api.Router { genreRepository := persistence.NewGenreRepository() browser := engine.NewBrowser(propertyRepository, mediaFolderRepository, artistRepository, albumRepository, mediaFileRepository, genreRepository) cover := engine.NewCover(mediaFileRepository, albumRepository) - nowPlayingRepository := persistence.NewNowPlayingRepository() + nowPlayingRepository := engine.NewNowPlayingRepository() listGenerator := engine.NewListGenerator(artistRepository, albumRepository, mediaFileRepository, nowPlayingRepository) itunesControl := itunesbridge.NewItunesControl() playlistRepository := persistence.NewPlaylistRepository()