navidrome/core/scrobbler/broker.go
2021-06-23 11:01:58 -04:00

127 lines
3.3 KiB
Go

package scrobbler
import (
"context"
"sort"
"time"
"github.com/navidrome/navidrome/log"
"github.com/ReneKroon/ttlcache/v2"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/singleton"
)
const nowPlayingExpire = 60 * time.Minute
type NowPlayingInfo struct {
TrackID string
Start time.Time
Username string
PlayerId string
PlayerName string
}
type Broker interface {
NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error
GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error)
Submit(ctx context.Context, trackId string, playTime time.Time) error
}
type broker struct {
ds model.DataStore
playMap *ttlcache.Cache
}
func GetBroker(ds model.DataStore) Broker {
instance := singleton.Get(broker{}, func() interface{} {
m := ttlcache.NewCache()
m.SkipTTLExtensionOnHit(true)
_ = m.SetTTL(nowPlayingExpire)
return &broker{ds: ds, playMap: m}
})
return instance.(*broker)
}
func (s *broker) NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error {
user, _ := request.UserFrom(ctx)
info := NowPlayingInfo{
TrackID: trackId,
Start: time.Now(),
Username: user.UserName,
PlayerId: playerId,
PlayerName: playerName,
}
_ = s.playMap.Set(playerId, info)
s.dispatchNowPlaying(ctx, user.ID, trackId)
return nil
}
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)
return
}
// 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.NowPlaying(ctx, userId, t)
}()
if err != nil {
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
return
}
}
}
func (s *broker) GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error) {
var res []NowPlayingInfo
for _, playerId := range s.playMap.GetKeys() {
value, err := s.playMap.Get(playerId)
if err != nil {
continue
}
info := value.(NowPlayingInfo)
res = append(res, info)
}
sort.Slice(res, func(i, j int) bool {
return res[i].Start.After(res[j].Start)
})
return res, nil
}
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
func Register(name string, init Constructor) {
if scrobblers == nil {
scrobblers = make(map[string]Constructor)
}
scrobblers[name] = init
}