diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index 4a5f03e..8ce6e43 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -1,7 +1,6 @@ package jellyfin import ( - "errors" "image" "io" "math" @@ -360,8 +359,14 @@ func (j *jellyfinMediaProvider) DownloadTrack(trackID string) (io.Reader, error) return resp.Body, nil } -func (j *jellyfinMediaProvider) Scrobble(trackID string, submission bool) error { - return errors.New("unimplemented") +func (j *jellyfinMediaProvider) ClientDecidesScrobble() bool { return false } + +func (j *jellyfinMediaProvider) TrackBeganPlayback(trackID string) error { + return j.client.UpdatePlayStatus(trackID, jellyfin.Start, 0) +} + +func (j *jellyfinMediaProvider) TrackEndedPlayback(trackID string, position int, submission bool) error { + return j.client.UpdatePlayStatus(trackID, jellyfin.Stop, int64(position)*runTimeTicksPerSecond) } func (j *jellyfinMediaProvider) RescanLibrary() error { diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index be7bc8e..0068df3 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -130,7 +130,14 @@ type MediaProvider interface { DeletePlaylist(id string) error - Scrobble(trackID string, submission bool) error + // True if the `submission` parameter to TrackEndedPlayback will be respected + // If false, the begin playback scrobble registers a play count immediately + // when TrackBeganPlayback is invoked. + ClientDecidesScrobble() bool + + TrackBeganPlayback(trackID string) error + + TrackEndedPlayback(trackID string, positionSecs int, submission bool) error DownloadTrack(trackID string) (io.Reader, error) diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 76055ec..44b51d2 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -256,10 +256,21 @@ func (s *subsonicMediaProvider) ReplacePlaylistTracks(playlistID string, trackID return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"playlistId": playlistID}) } -func (s *subsonicMediaProvider) Scrobble(trackID string, submission bool) error { +func (s *subsonicMediaProvider) ClientDecidesScrobble() bool { return true } + +func (s *subsonicMediaProvider) TrackBeganPlayback(trackID string) error { return s.client.Scrobble(trackID, map[string]string{ "time": strconv.FormatInt(time.Now().UnixMilli(), 10), - "submission": strconv.FormatBool(submission)}) + "submission": "false"}) +} + +func (s *subsonicMediaProvider) TrackEndedPlayback(trackID string, _ int, submission bool) error { + if !submission { + return nil + } + return s.client.Scrobble(trackID, map[string]string{ + "time": strconv.FormatInt(time.Now().UnixMilli(), 10), + "submission": "true"}) } func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 27fac46..a984f68 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -35,9 +35,10 @@ type PlaybackManager struct { sm *ServerManager player *player.Player - playTimeStopwatch util.Stopwatch - curTrackTime float64 - callbacksDisabled bool + playTimeStopwatch util.Stopwatch + curTrackTime float64 + latestTrackPosition float64 // cleared by checkScrobble + callbacksDisabled bool playQueue []*mediaprovider.Track nowPlayingIdx int64 @@ -73,15 +74,15 @@ func NewPlaybackManager( if tracknum >= int64(len(pm.playQueue)) { return } - pm.checkScrobble() + pm.checkScrobble() // scrobble the previous song if needed if pm.player.GetStatus().State == player.Playing { pm.playTimeStopwatch.Start() } pm.nowPlayingIdx = tracknum pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration) + pm.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled pm.invokeOnSongChangeCallbacks() pm.doUpdateTimePos() - pm.sendNowPlayingScrobble() }) p.OnSeek(func() { pm.doUpdateTimePos() @@ -380,13 +381,17 @@ func (p *PlaybackManager) checkScrobble() { pcnt := playDur.Seconds() / p.curTrackTime * 100 timeThresholdMet := p.scrobbleCfg.ThresholdTimeSeconds >= 0 && playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds) - if timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent) { - song := p.playQueue[p.nowPlayingIdx] - log.Printf("Scrobbling %q", song.Name) - song.PlayCount += 1 - p.lastScrobbled = song - go p.sm.Server.Scrobble(song.ID, true) + + track := p.playQueue[p.nowPlayingIdx] + var submission bool + server := p.sm.Server + if server.ClientDecidesScrobble() && (timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent)) { + track.PlayCount += 1 + p.lastScrobbled = track + submission = true } + go server.TrackEndedPlayback(track.ID, int(p.latestTrackPosition), submission) + p.latestTrackPosition = 0 p.playTimeStopwatch.Reset() } @@ -394,8 +399,14 @@ func (p *PlaybackManager) sendNowPlayingScrobble() { if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { return } - song := p.playQueue[p.nowPlayingIdx] - go p.sm.Server.Scrobble(song.ID, false) + track := p.playQueue[p.nowPlayingIdx] + server := p.sm.Server + if !server.ClientDecidesScrobble() { + // server will count track as scrobbled as soon as it starts playing + p.lastScrobbled = track + track.PlayCount += 1 + } + go p.sm.Server.TrackBeganPlayback(track.ID) } func (p *PlaybackManager) invokeOnSongChangeCallbacks() { @@ -433,6 +444,9 @@ func (p *PlaybackManager) doUpdateTimePos() { return } s := p.player.GetStatus() + if s.TimePos > p.latestTrackPosition { + p.latestTrackPosition = s.TimePos + } for _, cb := range p.onPlayTimeUpdate { cb(s.TimePos, s.Duration) } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 933379f..24f70c9 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -461,7 +461,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map } bands := c.App.Player.Equalizer().BandFrequencies() - dlg := dialogs.NewSettingsDialog(c.App.Config, devs, themeFiles, bands, c.MainWindow) + dlg := dialogs.NewSettingsDialog(c.App.Config, devs, themeFiles, bands, c.App.ServerManager.Server.ClientDecidesScrobble(), c.MainWindow) dlg.OnReplayGainSettingsChanged = func() { c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain) } diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 6d02860..c6e0396 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -43,6 +43,8 @@ type SettingsDialog struct { themeFiles map[string]string // filename -> displayName promptText *widget.RichText + clientDecidesScrobble bool + content fyne.CanvasObject } @@ -52,9 +54,10 @@ func NewSettingsDialog( audioDeviceList []player.AudioDevice, themeFileList map[string]string, equalizerBands []string, + clientDecidesScrobble bool, window fyne.Window, ) *SettingsDialog { - s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList} + s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList, clientDecidesScrobble: clientDecidesScrobble} s.ExtendBaseWidget(s) tabs := container.NewAppTabs( @@ -188,7 +191,9 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { durationEntry.Disable() } else { durationEntry.Text = lastScrobbleText - durationEntry.Enable() + if s.clientDecidesScrobble { + durationEntry.Enable() + } durationEntry.Refresh() durationEntry.OnChanged(durationEntry.Text) } @@ -197,6 +202,10 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { if !s.config.Scrobbling.Enabled { durationEnabled.Disable() } + if !s.clientDecidesScrobble { + percentEntry.Disable() + durationEnabled.Disable() + } scrobbleEnabled := widget.NewCheck("Send playback statistics to server", func(checked bool) { s.config.Scrobbling.Enabled = checked @@ -205,9 +214,11 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { durationEnabled.Disable() durationEntry.Disable() } else { - percentEntry.Enable() - durationEnabled.Enable() - if durationEnabled.Checked { + if s.clientDecidesScrobble { + percentEntry.Enable() + durationEnabled.Enable() + } + if durationEnabled.Checked && s.clientDecidesScrobble { durationEntry.Enable() } }