add support for OpenSubsonic playbackReport extension (#910)
* add support for OpenSubsonic playbackReport extension * better style
This commit is contained in:
@@ -302,6 +302,10 @@ type CanSavePlayQueue interface {
|
|||||||
GetPlayQueue() (*SavedPlayQueue, error)
|
GetPlayQueue() (*SavedPlayQueue, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CanReportPlayback interface {
|
||||||
|
ReportPlayback(trackID string, positionMs int64, state string) error
|
||||||
|
}
|
||||||
|
|
||||||
type LyricsProvider interface {
|
type LyricsProvider interface {
|
||||||
GetLyrics(track *Track) (*Lyrics, error)
|
GetLyrics(track *Track) (*Lyrics, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ type subsonicMediaProvider struct {
|
|||||||
|
|
||||||
radiosCached []*mediaprovider.RadioStation
|
radiosCached []*mediaprovider.RadioStation
|
||||||
radiosCachedAt int64 // unix
|
radiosCachedAt int64 // unix
|
||||||
|
|
||||||
|
playbackReportOnce sync.Once
|
||||||
|
playbackReportSupported bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
||||||
@@ -459,6 +462,30 @@ func (s *subsonicMediaProvider) GetLyrics(track *mediaprovider.Track) (*mediapro
|
|||||||
// CanSavePlayQueue interface
|
// CanSavePlayQueue interface
|
||||||
var _ mediaprovider.CanSavePlayQueue = (*subsonicMediaProvider)(nil)
|
var _ mediaprovider.CanSavePlayQueue = (*subsonicMediaProvider)(nil)
|
||||||
|
|
||||||
|
// CanReportPlayback interface
|
||||||
|
var _ mediaprovider.CanReportPlayback = (*subsonicMediaProvider)(nil)
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) ReportPlayback(trackID string, positionMs int64, state string) error {
|
||||||
|
s.playbackReportOnce.Do(func() {
|
||||||
|
ext, err := s.client.GetOpenSubsonicExtensions()
|
||||||
|
s.playbackReportSupported = err == nil && slices.ContainsFunc(ext,
|
||||||
|
func(e *subsonic.OpenSubsonicExtension) bool {
|
||||||
|
return e.Name == subsonic.PlaybackReport
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if !s.playbackReportSupported {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ignoreScrobble := true
|
||||||
|
return s.client.ReportPlayback(subsonic.ReportPlaybackParameters{
|
||||||
|
MediaID: trackID,
|
||||||
|
MediaType: subsonic.PlaybackMediaTypeSong,
|
||||||
|
PositionMs: positionMs,
|
||||||
|
State: subsonic.PlaybackState(state),
|
||||||
|
IgnoreScrobble: &ignoreScrobble,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) SavePlayQueue(trackIDs []string, currentTrackIdx int, timeSeconds int) error {
|
func (s *subsonicMediaProvider) SavePlayQueue(trackIDs []string, currentTrackIdx int, timeSeconds int) error {
|
||||||
if len(trackIDs) == 0 {
|
if len(trackIDs) == 0 {
|
||||||
return nil // don't save an empty queue
|
return nil // don't save an empty queue
|
||||||
|
|||||||
@@ -170,17 +170,24 @@ func (p *playbackEngine) registerPlayerCallbacks(pl player.BasePlayer) {
|
|||||||
pl.OnSeek(func() {
|
pl.OnSeek(func() {
|
||||||
p.handleTimePosUpdate(true)
|
p.handleTimePosUpdate(true)
|
||||||
p.invokeNoArgCallbacks(p.onSeek)
|
p.invokeNoArgCallbacks(p.onSeek)
|
||||||
|
seekState := "playing"
|
||||||
|
if p.PlaybackStatus().State == player.Paused {
|
||||||
|
seekState = "paused"
|
||||||
|
}
|
||||||
|
p.reportPlayback(seekState)
|
||||||
})
|
})
|
||||||
pl.OnStopped(p.handleOnStopped)
|
pl.OnStopped(p.handleOnStopped)
|
||||||
pl.OnPaused(func() {
|
pl.OnPaused(func() {
|
||||||
p.playTimeStopwatch.Stop()
|
p.playTimeStopwatch.Stop()
|
||||||
p.stopPollTimePos()
|
p.stopPollTimePos()
|
||||||
p.invokeNoArgCallbacks(p.onPaused)
|
p.invokeNoArgCallbacks(p.onPaused)
|
||||||
|
p.reportPlayback("paused")
|
||||||
})
|
})
|
||||||
pl.OnPlaying(func() {
|
pl.OnPlaying(func() {
|
||||||
p.playTimeStopwatch.Start()
|
p.playTimeStopwatch.Start()
|
||||||
p.startPollTimePos()
|
p.startPollTimePos()
|
||||||
p.invokeNoArgCallbacks(p.onPlaying)
|
p.invokeNoArgCallbacks(p.onPlaying)
|
||||||
|
p.reportPlayback("playing")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,6 +933,7 @@ func (p *playbackEngine) handleOnStopped() {
|
|||||||
p.handleTimePosUpdate(false)
|
p.handleTimePosUpdate(false)
|
||||||
p.invokeOnSongChangeCallbacks()
|
p.invokeOnSongChangeCallbacks()
|
||||||
p.invokeNoArgCallbacks(p.onStopped)
|
p.invokeNoArgCallbacks(p.onStopped)
|
||||||
|
p.reportPlayback("stopped")
|
||||||
p.alreadyScrobbled = false
|
p.alreadyScrobbled = false
|
||||||
p.wasStopped = true
|
p.wasStopped = true
|
||||||
p.nowPlayingIdx = -1
|
p.nowPlayingIdx = -1
|
||||||
@@ -1091,6 +1099,7 @@ func (p *playbackEngine) sendNowPlayingScrobble() {
|
|||||||
track.PlayCount += 1
|
track.PlayCount += 1
|
||||||
}
|
}
|
||||||
go p.sm.Server.TrackBeganPlayback(track.ID)
|
go p.sm.Server.TrackBeganPlayback(track.ID)
|
||||||
|
p.reportPlayback("starting")
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates a deep copy of the track info so that we can maintain our own state
|
// creates a deep copy of the track info so that we can maintain our own state
|
||||||
@@ -1113,6 +1122,22 @@ func (p *playbackEngine) invokeOnSongChangeCallbacks() {
|
|||||||
p.lastScrobbled = nil
|
p.lastScrobbled = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *playbackEngine) reportPlayback(state string) {
|
||||||
|
reporter, ok := p.sm.Server.(mediaprovider.CanReportPlayback)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
np := p.NowPlaying()
|
||||||
|
if np == nil || np.Metadata().Type != mediaprovider.MediaItemTypeTrack {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
posMs := int64(p.latestTrackPosition * 1000)
|
||||||
|
go reporter.ReportPlayback(np.Metadata().ID, posMs, state)
|
||||||
|
}
|
||||||
|
|
||||||
func (pm *playbackEngine) invokeNoArgCallbacks(cbs []func()) {
|
func (pm *playbackEngine) invokeNoArgCallbacks(cbs []func()) {
|
||||||
if pm.callbacksDisabled {
|
if pm.callbacksDisabled {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ require (
|
|||||||
github.com/quarckster/go-mpris-server v1.0.3
|
github.com/quarckster/go-mpris-server v1.0.3
|
||||||
github.com/supersonic-app/fyne-lyrics v0.0.0-20250614151306-b1880a70a410
|
github.com/supersonic-app/fyne-lyrics v0.0.0-20250614151306-b1880a70a410
|
||||||
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449
|
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20260409151555-0d5fb7d4801a
|
github.com/supersonic-app/go-subsonic v0.0.0-20260416152144-7a5f505a273c
|
||||||
github.com/supersonic-app/go-upnpcast v0.1.0
|
github.com/supersonic-app/go-upnpcast v0.1.0
|
||||||
github.com/zalando/go-keyring v0.2.8
|
github.com/zalando/go-keyring v0.2.8
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/net v0.50.0
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449 h1:UHIPI43
|
|||||||
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449/go.mod h1:1bQz6kBQumJopXEbkiqoLxIXLy7F7yWFBvknvpAtIC0=
|
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449/go.mod h1:1bQz6kBQumJopXEbkiqoLxIXLy7F7yWFBvknvpAtIC0=
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20260409151555-0d5fb7d4801a h1:zK5ZbE7H337udw2tOZQlnhGv7URSxfV1AIvRpKdHG2E=
|
github.com/supersonic-app/go-subsonic v0.0.0-20260409151555-0d5fb7d4801a h1:zK5ZbE7H337udw2tOZQlnhGv7URSxfV1AIvRpKdHG2E=
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20260409151555-0d5fb7d4801a/go.mod h1:ClhAgC2qobwH19D6kdwULLYSHlOmldDGkdu2G2Z0Ijo=
|
github.com/supersonic-app/go-subsonic v0.0.0-20260409151555-0d5fb7d4801a/go.mod h1:ClhAgC2qobwH19D6kdwULLYSHlOmldDGkdu2G2Z0Ijo=
|
||||||
|
github.com/supersonic-app/go-subsonic v0.0.0-20260416152144-7a5f505a273c h1:wms3FvahSfcnacW2rs9L+vzJR8FmTE5xTrRgf3+k3eQ=
|
||||||
|
github.com/supersonic-app/go-subsonic v0.0.0-20260416152144-7a5f505a273c/go.mod h1:ClhAgC2qobwH19D6kdwULLYSHlOmldDGkdu2G2Z0Ijo=
|
||||||
github.com/supersonic-app/go-upnpcast v0.1.0 h1:SL0WVgnqvAVaLT9MSRhSeVwF9pcUw1cgBITKVoWXN3g=
|
github.com/supersonic-app/go-upnpcast v0.1.0 h1:SL0WVgnqvAVaLT9MSRhSeVwF9pcUw1cgBITKVoWXN3g=
|
||||||
github.com/supersonic-app/go-upnpcast v0.1.0/go.mod h1:o/QGDTK3qdaS3h9Bt8aH5ywgbWbrPVzPCHlZYFs0IHY=
|
github.com/supersonic-app/go-upnpcast v0.1.0/go.mod h1:o/QGDTK3qdaS3h9Bt8aH5ywgbWbrPVzPCHlZYFs0IHY=
|
||||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
|
|||||||
Reference in New Issue
Block a user