From 923d8ddbe984fc36ca331ebecb708800503ca894 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 30 Dec 2022 17:45:57 -0800 Subject: [PATCH] add scrobbling of played tracks --- backend/playbackmanager.go | 35 +++++++++++++++++++++++++++++++++++ backend/util/stopwatch.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 backend/util/stopwatch.go diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 9358694..d0faffa 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -2,12 +2,19 @@ package backend import ( "context" + "log" + "strconv" + "supersonic/backend/util" "supersonic/player" "time" subsonic "github.com/dweymouth/go-subsonic" ) +const ( + ScrobbleThreshold = 0.6 +) + type PlaybackManager struct { ctx context.Context cancelPollPos context.CancelFunc @@ -15,6 +22,9 @@ type PlaybackManager struct { sm *ServerManager player *player.Player + playTimeStopwatch util.Stopwatch + curTrackTime float64 + playQueue []*subsonic.Child nowPlayingIdx int64 onSongChange []func(*subsonic.Child) @@ -27,11 +37,20 @@ func NewPlaybackManager(ctx context.Context, s *ServerManager, p *player.Player) sm: s, player: p, } + // TODO: we get some spurious OnTrackChange callbacks from the player, + // especially when loading/playing a new album, + // but for now they're pretty much harmless. Investigate later. p.OnTrackChange(func(tracknum int64) { if tracknum >= int64(len(pm.playQueue)) { return } + pm.checkScrobble(pm.playTimeStopwatch.Elapsed()) + pm.playTimeStopwatch.Reset() + if pm.player.GetStatus().State == player.Playing { + pm.playTimeStopwatch.Start() + } pm.nowPlayingIdx = tracknum + pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration) for _, cb := range pm.onSongChange { cb(pm.NowPlaying()) } @@ -41,15 +60,20 @@ func NewPlaybackManager(ctx context.Context, s *ServerManager, p *player.Player) pm.doUpdateTimePos() }) p.OnStopped(func() { + pm.playTimeStopwatch.Stop() + pm.checkScrobble(pm.playTimeStopwatch.Elapsed()) + pm.playTimeStopwatch.Reset() pm.stopPollTimePos() for _, cb := range pm.onSongChange { cb(nil) } }) p.OnPaused(func() { + pm.playTimeStopwatch.Stop() pm.stopPollTimePos() }) p.OnPlaying(func() { + pm.playTimeStopwatch.Start() pm.startPollTimePos() }) @@ -107,6 +131,17 @@ func (p *PlaybackManager) PlayAlbum(albumID string) error { return p.player.PlayFromBeginning() } +func (p *PlaybackManager) checkScrobble(playDur time.Duration) { + if playDur.Seconds() < 0.1 || p.curTrackTime < 0.1 { + return + } + song := p.playQueue[p.nowPlayingIdx] + if playDur.Seconds()/p.curTrackTime > ScrobbleThreshold { + log.Printf("Scrobbling %q", song.Title) + p.sm.Server.Scrobble(song.ID, map[string]string{"time": strconv.FormatInt(time.Now().Unix()*1000, 10)}) + } +} + func (p *PlaybackManager) startPollTimePos() { ctx, cancel := context.WithCancel(p.ctx) p.cancelPollPos = cancel diff --git a/backend/util/stopwatch.go b/backend/util/stopwatch.go new file mode 100644 index 0000000..f3610a2 --- /dev/null +++ b/backend/util/stopwatch.go @@ -0,0 +1,38 @@ +package util + +import "time" + +type Stopwatch struct { + running bool + started time.Time + elapsed time.Duration +} + +func (s *Stopwatch) Start() { + if s.running { + return + } + s.started = time.Now() + s.running = true +} + +func (s *Stopwatch) Stop() { + if !s.running { + return + } + s.elapsed += time.Since(s.started) + s.running = false +} + +func (s *Stopwatch) Elapsed() time.Duration { + e := s.elapsed + if s.running { + e += time.Since(s.started) + } + return e +} + +func (s *Stopwatch) Reset() { + s.running = false + s.elapsed = time.Duration(0) +}