add scrobbling for Jellyfin

This commit is contained in:
Drew Weymouth
2023-11-14 18:08:55 -08:00
parent b7f54054d8
commit 2ffcac9b88
6 changed files with 73 additions and 25 deletions
@@ -1,7 +1,6 @@
package jellyfin package jellyfin
import ( import (
"errors"
"image" "image"
"io" "io"
"math" "math"
@@ -360,8 +359,14 @@ func (j *jellyfinMediaProvider) DownloadTrack(trackID string) (io.Reader, error)
return resp.Body, nil return resp.Body, nil
} }
func (j *jellyfinMediaProvider) Scrobble(trackID string, submission bool) error { func (j *jellyfinMediaProvider) ClientDecidesScrobble() bool { return false }
return errors.New("unimplemented")
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 { func (j *jellyfinMediaProvider) RescanLibrary() error {
+8 -1
View File
@@ -130,7 +130,14 @@ type MediaProvider interface {
DeletePlaylist(id string) error 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) DownloadTrack(trackID string) (io.Reader, error)
@@ -256,10 +256,21 @@ func (s *subsonicMediaProvider) ReplacePlaylistTracks(playlistID string, trackID
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"playlistId": playlistID}) 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{ return s.client.Scrobble(trackID, map[string]string{
"time": strconv.FormatInt(time.Now().UnixMilli(), 10), "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 { func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
+27 -13
View File
@@ -35,9 +35,10 @@ type PlaybackManager struct {
sm *ServerManager sm *ServerManager
player *player.Player player *player.Player
playTimeStopwatch util.Stopwatch playTimeStopwatch util.Stopwatch
curTrackTime float64 curTrackTime float64
callbacksDisabled bool latestTrackPosition float64 // cleared by checkScrobble
callbacksDisabled bool
playQueue []*mediaprovider.Track playQueue []*mediaprovider.Track
nowPlayingIdx int64 nowPlayingIdx int64
@@ -73,15 +74,15 @@ func NewPlaybackManager(
if tracknum >= int64(len(pm.playQueue)) { if tracknum >= int64(len(pm.playQueue)) {
return return
} }
pm.checkScrobble() pm.checkScrobble() // scrobble the previous song if needed
if pm.player.GetStatus().State == player.Playing { if pm.player.GetStatus().State == player.Playing {
pm.playTimeStopwatch.Start() pm.playTimeStopwatch.Start()
} }
pm.nowPlayingIdx = tracknum pm.nowPlayingIdx = tracknum
pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration) pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration)
pm.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled
pm.invokeOnSongChangeCallbacks() pm.invokeOnSongChangeCallbacks()
pm.doUpdateTimePos() pm.doUpdateTimePos()
pm.sendNowPlayingScrobble()
}) })
p.OnSeek(func() { p.OnSeek(func() {
pm.doUpdateTimePos() pm.doUpdateTimePos()
@@ -380,13 +381,17 @@ func (p *PlaybackManager) checkScrobble() {
pcnt := playDur.Seconds() / p.curTrackTime * 100 pcnt := playDur.Seconds() / p.curTrackTime * 100
timeThresholdMet := p.scrobbleCfg.ThresholdTimeSeconds >= 0 && timeThresholdMet := p.scrobbleCfg.ThresholdTimeSeconds >= 0 &&
playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds) playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds)
if timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent) {
song := p.playQueue[p.nowPlayingIdx] track := p.playQueue[p.nowPlayingIdx]
log.Printf("Scrobbling %q", song.Name) var submission bool
song.PlayCount += 1 server := p.sm.Server
p.lastScrobbled = song if server.ClientDecidesScrobble() && (timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent)) {
go p.sm.Server.Scrobble(song.ID, true) track.PlayCount += 1
p.lastScrobbled = track
submission = true
} }
go server.TrackEndedPlayback(track.ID, int(p.latestTrackPosition), submission)
p.latestTrackPosition = 0
p.playTimeStopwatch.Reset() p.playTimeStopwatch.Reset()
} }
@@ -394,8 +399,14 @@ func (p *PlaybackManager) sendNowPlayingScrobble() {
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
return return
} }
song := p.playQueue[p.nowPlayingIdx] track := p.playQueue[p.nowPlayingIdx]
go p.sm.Server.Scrobble(song.ID, false) 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() { func (p *PlaybackManager) invokeOnSongChangeCallbacks() {
@@ -433,6 +444,9 @@ func (p *PlaybackManager) doUpdateTimePos() {
return return
} }
s := p.player.GetStatus() s := p.player.GetStatus()
if s.TimePos > p.latestTrackPosition {
p.latestTrackPosition = s.TimePos
}
for _, cb := range p.onPlayTimeUpdate { for _, cb := range p.onPlayTimeUpdate {
cb(s.TimePos, s.Duration) cb(s.TimePos, s.Duration)
} }
+1 -1
View File
@@ -461,7 +461,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
} }
bands := c.App.Player.Equalizer().BandFrequencies() 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() { dlg.OnReplayGainSettingsChanged = func() {
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain) c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
} }
+16 -5
View File
@@ -43,6 +43,8 @@ type SettingsDialog struct {
themeFiles map[string]string // filename -> displayName themeFiles map[string]string // filename -> displayName
promptText *widget.RichText promptText *widget.RichText
clientDecidesScrobble bool
content fyne.CanvasObject content fyne.CanvasObject
} }
@@ -52,9 +54,10 @@ func NewSettingsDialog(
audioDeviceList []player.AudioDevice, audioDeviceList []player.AudioDevice,
themeFileList map[string]string, themeFileList map[string]string,
equalizerBands []string, equalizerBands []string,
clientDecidesScrobble bool,
window fyne.Window, window fyne.Window,
) *SettingsDialog { ) *SettingsDialog {
s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList} s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList, clientDecidesScrobble: clientDecidesScrobble}
s.ExtendBaseWidget(s) s.ExtendBaseWidget(s)
tabs := container.NewAppTabs( tabs := container.NewAppTabs(
@@ -188,7 +191,9 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
durationEntry.Disable() durationEntry.Disable()
} else { } else {
durationEntry.Text = lastScrobbleText durationEntry.Text = lastScrobbleText
durationEntry.Enable() if s.clientDecidesScrobble {
durationEntry.Enable()
}
durationEntry.Refresh() durationEntry.Refresh()
durationEntry.OnChanged(durationEntry.Text) durationEntry.OnChanged(durationEntry.Text)
} }
@@ -197,6 +202,10 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
if !s.config.Scrobbling.Enabled { if !s.config.Scrobbling.Enabled {
durationEnabled.Disable() durationEnabled.Disable()
} }
if !s.clientDecidesScrobble {
percentEntry.Disable()
durationEnabled.Disable()
}
scrobbleEnabled := widget.NewCheck("Send playback statistics to server", func(checked bool) { scrobbleEnabled := widget.NewCheck("Send playback statistics to server", func(checked bool) {
s.config.Scrobbling.Enabled = checked s.config.Scrobbling.Enabled = checked
@@ -205,9 +214,11 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
durationEnabled.Disable() durationEnabled.Disable()
durationEntry.Disable() durationEntry.Disable()
} else { } else {
percentEntry.Enable() if s.clientDecidesScrobble {
durationEnabled.Enable() percentEntry.Enable()
if durationEnabled.Checked { durationEnabled.Enable()
}
if durationEnabled.Checked && s.clientDecidesScrobble {
durationEntry.Enable() durationEntry.Enable()
} }
} }