188 lines
5.6 KiB
Go
188 lines
5.6 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/playback"
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
"github.com/Silo-Server/silo-server/internal/watchsync"
|
|
)
|
|
|
|
type compatScrobbleAction string
|
|
|
|
const (
|
|
compatScrobbleStart compatScrobbleAction = "start"
|
|
compatScrobblePause compatScrobbleAction = "pause"
|
|
compatScrobbleStop compatScrobbleAction = "stop"
|
|
)
|
|
|
|
func (h *PlaybackHandler) dispatchCompatScrobble(
|
|
ctx context.Context,
|
|
action compatScrobbleAction,
|
|
playSession *PlaybackSession,
|
|
upstreamSession *playback.Session,
|
|
preferredSource *PlaybackMediaSource,
|
|
) error {
|
|
return h.dispatchCompatScrobbleAt(ctx, action, playSession, upstreamSession, preferredSource, nil)
|
|
}
|
|
|
|
func (h *PlaybackHandler) dispatchCompatScrobbleAt(
|
|
ctx context.Context,
|
|
action compatScrobbleAction,
|
|
playSession *PlaybackSession,
|
|
upstreamSession *playback.Session,
|
|
preferredSource *PlaybackMediaSource,
|
|
positionOverride *float64,
|
|
) error {
|
|
event, ok := h.compatScrobbleEvent(
|
|
ctx, action, playSession, upstreamSession, preferredSource, positionOverride,
|
|
)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return h.dispatchCompatScrobbleEvent(ctx, action, event)
|
|
}
|
|
|
|
func (h *PlaybackHandler) compatScrobbleEvent(
|
|
ctx context.Context,
|
|
action compatScrobbleAction,
|
|
playSession *PlaybackSession,
|
|
upstreamSession *playback.Session,
|
|
preferredSource *PlaybackMediaSource,
|
|
positionOverride *float64,
|
|
) (watchsync.ScrobbleEvent, bool) {
|
|
if h == nil || h.WatchScrobbler == nil || playSession == nil || upstreamSession == nil ||
|
|
upstreamSession.DisableProgressPersistence || playSession.ItemID == "" {
|
|
return watchsync.ScrobbleEvent{}, false
|
|
}
|
|
scrobbleCtx, cancel := compatDetachedContext(ctx)
|
|
defer cancel()
|
|
|
|
source := compatScrobbleSource(playSession, upstreamSession, preferredSource)
|
|
duration := 0.0
|
|
if source != nil {
|
|
duration = float64(source.Version.Duration)
|
|
}
|
|
position := upstreamSession.Position
|
|
if positionOverride != nil {
|
|
position = *positionOverride
|
|
} else if position <= 0 && playSession.InitialSeekSeconds > 0 {
|
|
position = playSession.InitialSeekSeconds
|
|
}
|
|
completed := false
|
|
if action == compatScrobbleStop && duration > 0 {
|
|
_, completed, _ = userstore.ResolveProgressState(position, duration, h.playbackThresholds(scrobbleCtx))
|
|
}
|
|
event := watchsync.ResolveScrobbleIdentity(scrobbleCtx, h.StableIdentityResolver, watchsync.ScrobbleEvent{
|
|
PlaybackSessionID: upstreamSession.ID,
|
|
UserID: upstreamSession.UserID,
|
|
ProfileID: upstreamSession.ProfileID,
|
|
MediaItemID: playSession.ItemID,
|
|
PositionSeconds: position,
|
|
DurationSeconds: duration,
|
|
Completed: completed,
|
|
OccurredAt: time.Now().UTC(),
|
|
})
|
|
return event, true
|
|
}
|
|
|
|
func (h *PlaybackHandler) dispatchCompatScrobbleEvent(
|
|
ctx context.Context,
|
|
action compatScrobbleAction,
|
|
event watchsync.ScrobbleEvent,
|
|
) error {
|
|
return h.dispatchCompatScrobbleEventConfirmed(ctx, action, event, false)
|
|
}
|
|
|
|
func (h *PlaybackHandler) dispatchCompatScrobbleEventConfirmed(
|
|
ctx context.Context,
|
|
action compatScrobbleAction,
|
|
event watchsync.ScrobbleEvent,
|
|
confirmStop bool,
|
|
) error {
|
|
if h == nil || h.WatchScrobbler == nil {
|
|
return nil
|
|
}
|
|
scrobbleCtx, cancel := compatDetachedContext(ctx)
|
|
defer cancel()
|
|
var err error
|
|
switch action {
|
|
case compatScrobblePause:
|
|
err = h.WatchScrobbler.ScrobblePause(scrobbleCtx, event)
|
|
case compatScrobbleStop:
|
|
if confirmer, ok := h.WatchScrobbler.(PlaybackWatchStopConfirmer); confirmStop && ok {
|
|
err = confirmer.ScrobbleStopConfirmed(scrobbleCtx, event)
|
|
} else if confirmStop {
|
|
err = errors.New("watch scrobbler does not support confirmed stops")
|
|
} else {
|
|
err = h.WatchScrobbler.ScrobbleStop(scrobbleCtx, event)
|
|
}
|
|
default:
|
|
err = h.WatchScrobbler.ScrobbleStart(scrobbleCtx, event)
|
|
}
|
|
if err != nil {
|
|
slog.WarnContext(scrobbleCtx, "failed to queue jellycompat watch provider scrobble",
|
|
"component", "jellycompat",
|
|
"action", action,
|
|
"playback_session_id", event.PlaybackSessionID,
|
|
"error", err,
|
|
)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func compatScrobbleSource(
|
|
playSession *PlaybackSession,
|
|
upstreamSession *playback.Session,
|
|
preferredSource *PlaybackMediaSource,
|
|
) *PlaybackMediaSource {
|
|
if preferredSource != nil {
|
|
return preferredSource
|
|
}
|
|
if playSession == nil {
|
|
return nil
|
|
}
|
|
if upstreamSession != nil {
|
|
for _, source := range playSession.MediaSources {
|
|
if source.FileID == upstreamSession.MediaFileID {
|
|
copy := source
|
|
return ©
|
|
}
|
|
}
|
|
}
|
|
return firstMediaSource(playSession)
|
|
}
|
|
|
|
// compatScrobbleFallbackSession preserves enough authenticated report state to
|
|
// emit a terminal event after the in-memory upstream session has already been
|
|
// reaped. The compat play session remains the source of media identity.
|
|
func compatScrobbleFallbackSession(
|
|
compatSession *Session,
|
|
playSession *PlaybackSession,
|
|
preferredSource *PlaybackMediaSource,
|
|
position float64,
|
|
positionKnown bool,
|
|
isPaused bool,
|
|
) *playback.Session {
|
|
if compatSession == nil || playSession == nil || playSession.UpstreamSessionID == "" || !positionKnown {
|
|
return nil
|
|
}
|
|
source := compatScrobbleSource(playSession, nil, preferredSource)
|
|
fileID := 0
|
|
if source != nil {
|
|
fileID = source.FileID
|
|
}
|
|
return &playback.Session{
|
|
ID: playSession.UpstreamSessionID,
|
|
UserID: compatSession.StreamAppUserID,
|
|
ProfileID: compatSession.ProfileID,
|
|
MediaFileID: fileID,
|
|
Position: position,
|
|
IsPaused: isPaused,
|
|
DisableProgressPersistence: !playSession.ProgressPersistenceKnown || playSession.DisableProgressPersistence,
|
|
}
|
|
}
|