Send NowPlaying and Scrobbles to Last.fm
This commit is contained in:
+27
-10
@@ -26,7 +26,7 @@ type NowPlayingInfo struct {
|
||||
type Broker interface {
|
||||
NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error
|
||||
GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error)
|
||||
Submit(ctx context.Context, playerId int, trackId string, playTime time.Time) error
|
||||
Submit(ctx context.Context, trackId string, playTime time.Time) error
|
||||
}
|
||||
|
||||
type broker struct {
|
||||
@@ -45,20 +45,20 @@ func GetBroker(ds model.DataStore) Broker {
|
||||
}
|
||||
|
||||
func (s *broker) NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error {
|
||||
username, _ := request.UsernameFrom(ctx)
|
||||
user, _ := request.UserFrom(ctx)
|
||||
info := NowPlayingInfo{
|
||||
TrackID: trackId,
|
||||
Start: time.Now(),
|
||||
Username: username,
|
||||
Username: user.UserName,
|
||||
PlayerId: playerId,
|
||||
PlayerName: playerName,
|
||||
}
|
||||
_ = s.playMap.Set(playerId, info)
|
||||
s.dispatchNowPlaying(ctx, trackId)
|
||||
s.dispatchNowPlaying(ctx, user.ID, trackId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *broker) dispatchNowPlaying(ctx context.Context, trackId string) {
|
||||
func (s *broker) dispatchNowPlaying(ctx context.Context, userId string, trackId string) {
|
||||
t, err := s.ds.MediaFile(ctx).Get(trackId)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
|
||||
@@ -68,10 +68,8 @@ func (s *broker) dispatchNowPlaying(ctx context.Context, trackId string) {
|
||||
for name, constructor := range scrobblers {
|
||||
log.Debug(ctx, "Sending NowPlaying info", "scrobbler", name, "track", t.Title, "artist", t.Artist)
|
||||
err := func() error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
s := constructor(s.ds)
|
||||
return s.NowPlaying(ctx, t)
|
||||
return s.NowPlaying(ctx, userId, t)
|
||||
}()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
|
||||
@@ -96,8 +94,27 @@ func (s *broker) GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *broker) Submit(ctx context.Context, playerId int, trackId string, playTime time.Time) error {
|
||||
panic("implement me")
|
||||
func (s *broker) Submit(ctx context.Context, trackId string, playTime time.Time) error {
|
||||
u, _ := request.UserFrom(ctx)
|
||||
t, err := s.ds.MediaFile(ctx).Get(trackId)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
|
||||
return err
|
||||
}
|
||||
scrobbles := []Scrobble{{MediaFile: *t, TimeStamp: playTime}}
|
||||
// TODO Parallelize
|
||||
for name, constructor := range scrobblers {
|
||||
log.Debug(ctx, "Sending NowPlaying info", "scrobbler", name, "track", t.Title, "artist", t.Artist)
|
||||
err := func() error {
|
||||
s := constructor(s.ds)
|
||||
return s.Scrobble(ctx, u.ID, scrobbles)
|
||||
}()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var scrobblers map[string]Constructor
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package scrobbler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Broker", func() {
|
||||
var ctx context.Context
|
||||
var ds model.DataStore
|
||||
var broker Broker
|
||||
var track model.MediaFile
|
||||
var fake *fakeScrobbler
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
ctx = request.WithUser(ctx, model.User{ID: "u-1"})
|
||||
ds = &tests.MockDataStore{}
|
||||
broker = GetBroker(ds)
|
||||
fake = &fakeScrobbler{}
|
||||
Register("fake", func(ds model.DataStore) Scrobbler {
|
||||
return fake
|
||||
})
|
||||
|
||||
track = model.MediaFile{
|
||||
ID: "123",
|
||||
Title: "Track Title",
|
||||
Album: "Track Album",
|
||||
Artist: "Track Artist",
|
||||
AlbumArtist: "Track AlbumArtist",
|
||||
TrackNumber: 1,
|
||||
Duration: 180,
|
||||
MbzTrackID: "mbz-123",
|
||||
}
|
||||
_ = ds.MediaFile(ctx).Put(&track)
|
||||
})
|
||||
|
||||
Describe("NowPlaying", func() {
|
||||
It("sends track to agent", func() {
|
||||
err := broker.NowPlaying(ctx, "player-1", "player-one", "123")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(fake.UserID).To(Equal("u-1"))
|
||||
Expect(fake.Track.ID).To(Equal("123"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetNowPlaying", func() {
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
})
|
||||
It("returns current playing music", func() {
|
||||
track2 := track
|
||||
track2.ID = "456"
|
||||
_ = ds.MediaFile(ctx).Put(&track)
|
||||
ctx = request.WithUser(ctx, model.User{UserName: "user-1"})
|
||||
_ = broker.NowPlaying(ctx, "player-1", "player-one", "123")
|
||||
ctx = request.WithUser(ctx, model.User{UserName: "user-2"})
|
||||
_ = broker.NowPlaying(ctx, "player-2", "player-two", "456")
|
||||
|
||||
playing, err := broker.GetNowPlaying(ctx)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(playing).To(HaveLen(2))
|
||||
Expect(playing[0].PlayerId).To(Equal("player-2"))
|
||||
Expect(playing[0].PlayerName).To(Equal("player-two"))
|
||||
Expect(playing[0].Username).To(Equal("user-2"))
|
||||
Expect(playing[0].TrackID).To(Equal("456"))
|
||||
|
||||
Expect(playing[1].PlayerId).To(Equal("player-1"))
|
||||
Expect(playing[1].PlayerName).To(Equal("player-one"))
|
||||
Expect(playing[1].Username).To(Equal("user-1"))
|
||||
Expect(playing[1].TrackID).To(Equal("123"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Submit", func() {
|
||||
It("sends track to agent", func() {
|
||||
ctx = request.WithUser(ctx, model.User{ID: "u-1", UserName: "user-1"})
|
||||
ts := time.Now()
|
||||
|
||||
err := broker.Submit(ctx, "123", ts)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(fake.UserID).To(Equal("u-1"))
|
||||
Expect(fake.Scrobbles[0].ID).To(Equal("123"))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
type fakeScrobbler struct {
|
||||
UserID string
|
||||
Track *model.MediaFile
|
||||
Scrobbles []Scrobble
|
||||
Error error
|
||||
}
|
||||
|
||||
func (f *fakeScrobbler) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
|
||||
if f.Error != nil {
|
||||
return f.Error
|
||||
}
|
||||
f.UserID = userId
|
||||
f.Track = track
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeScrobbler) Scrobble(ctx context.Context, userId string, scrobbles []Scrobble) error {
|
||||
if f.Error != nil {
|
||||
return f.Error
|
||||
}
|
||||
f.UserID = userId
|
||||
f.Scrobbles = scrobbles
|
||||
return nil
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
)
|
||||
|
||||
type Scrobble struct {
|
||||
Track *model.MediaFile
|
||||
TimeStamp *time.Time
|
||||
model.MediaFile
|
||||
TimeStamp time.Time
|
||||
}
|
||||
|
||||
type Scrobbler interface {
|
||||
NowPlaying(context.Context, *model.MediaFile) error
|
||||
Scrobble(context.Context, []Scrobble) error
|
||||
NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error
|
||||
Scrobble(ctx context.Context, userId string, scrobbles []Scrobble) error
|
||||
}
|
||||
|
||||
type Constructor func(ds model.DataStore) Scrobbler
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package scrobbler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestAgents(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
log.SetLevel(log.LevelCritical)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Scrobbler Test Suite")
|
||||
}
|
||||
Reference in New Issue
Block a user