mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
127 lines
3.3 KiB
Go
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
|
|
}
|