add scrobbling for Jellyfin
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+27
-13
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user