Files
silo-server/internal/jellycompat/playback_sessions.go
T

153 lines
3.7 KiB
Go

package jellycompat
import (
"sync"
"time"
"github.com/Silo-Server/silo-server/internal/catalog"
)
// PlaybackSession stores compat-owned playback negotiation state before the
// native Silo playback session starts.
type PlaybackSession struct {
ID string
CompatToken string
ItemID string
RouteItemID string
UserID string
InitialSeekSeconds float64
MediaSources []PlaybackMediaSource
UpstreamSessionID string
UpstreamPlayMethod string
TranscodeStarted bool
CreatedAt time.Time
UpdatedAt time.Time
ExpiresAt time.Time
}
// PlaybackMediaSource stores one negotiated stream source within a compat play session.
type PlaybackMediaSource struct {
ID string
FileID int
Version catalog.FileVersion
SupportsDirectPlay bool
SupportsDirectStream bool
SupportsTranscoding bool
TranscodeAudio bool
DefaultAudioStreamIndex *int
SelectedAudioStreamIndex *int
DefaultSubtitleStreamIndex *int
ETag string
}
// PlaybackSessionStore keeps compat playback sessions in memory.
type PlaybackSessionStore struct {
mu sync.RWMutex
sessions map[string]PlaybackSession
ttl time.Duration
now func() time.Time
}
// NewPlaybackSessionStore creates a new playback session store.
func NewPlaybackSessionStore(ttl time.Duration, now func() time.Time) *PlaybackSessionStore {
if now == nil {
now = time.Now
}
if ttl <= 0 {
ttl = 6 * time.Hour
}
return &PlaybackSessionStore{
sessions: make(map[string]PlaybackSession),
ttl: ttl,
now: now,
}
}
// Put stores or replaces a compat playback session.
func (s *PlaybackSessionStore) Put(session PlaybackSession) {
s.mu.Lock()
defer s.mu.Unlock()
if session.CreatedAt.IsZero() {
session.CreatedAt = s.now()
}
session.UpdatedAt = s.now()
if session.ExpiresAt.IsZero() {
session.ExpiresAt = session.CreatedAt.Add(s.ttl)
}
s.sessions[session.ID] = session
}
// Get returns a playback session when it exists and is not expired.
func (s *PlaybackSessionStore) Get(id string) (*PlaybackSession, bool) {
s.mu.RLock()
session, ok := s.sessions[id]
s.mu.RUnlock()
if !ok {
return nil, false
}
if !session.ExpiresAt.After(s.now()) {
s.Delete(id)
return nil, false
}
cp := session
return &cp, true
}
// Delete removes a playback session.
func (s *PlaybackSessionStore) Delete(id string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.sessions, id)
}
// Update modifies a playback session in place.
func (s *PlaybackSessionStore) Update(id string, fn func(*PlaybackSession) error) error {
s.mu.Lock()
defer s.mu.Unlock()
session, ok := s.sessions[id]
if !ok {
return ErrSessionNotFound
}
if !session.ExpiresAt.After(s.now()) {
delete(s.sessions, id)
return ErrSessionNotFound
}
if err := fn(&session); err != nil {
return err
}
session.UpdatedAt = s.now()
s.sessions[id] = session
return nil
}
// FindByRoute resolves a route item/media-source identifier to a compat playback session.
func (s *PlaybackSessionStore) FindByRoute(compatToken, routeID string) (*PlaybackSession, *PlaybackMediaSource, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
now := s.now()
for _, session := range s.sessions {
if !session.ExpiresAt.After(now) {
continue
}
if compatToken != "" && session.CompatToken != compatToken {
continue
}
if session.RouteItemID == routeID {
cp := session
return &cp, nil, true
}
for _, source := range session.MediaSources {
if source.ID == routeID {
cp := session
sourceCopy := source
return &cp, &sourceCopy, true
}
}
}
return nil, nil, false
}