Skip to content

Commit b2e72b9

Browse files
committed
plexautolang: cache episode information to avoid redundant api calls
1 parent ab690d0 commit b2e72b9

File tree

2 files changed

+133
-31
lines changed

2 files changed

+133
-31
lines changed

internal/plexautolang/cache.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ type MachineIdentifier struct {
3131
CachedAt time.Time
3232
}
3333

34+
// SessionEpisode caches episode metadata for an active session
35+
type SessionEpisode struct {
36+
Episode *Episode
37+
RatingKey string
38+
CachedAt time.Time
39+
}
40+
3441
// Cache holds in-memory caches for Plex Auto Languages
3542
type Cache struct {
3643
mu sync.RWMutex
@@ -61,6 +68,9 @@ type Cache struct {
6168
// lastProcessed tracks when we last processed a client/ratingKey combo
6269
// Key: "clientIdentifier:ratingKey"
6370
lastProcessed map[string]time.Time
71+
72+
// sessionEpisodes caches episode metadata per session key
73+
sessionEpisodes map[string]*SessionEpisode
6474
}
6575

6676
// NewCache creates a new cache instance
@@ -73,6 +83,7 @@ func NewCache() *Cache {
7383
newlyAdded: make(map[string]time.Time),
7484
recentActivities: make(map[string]time.Time),
7585
lastProcessed: make(map[string]time.Time),
86+
sessionEpisodes: make(map[string]*SessionEpisode),
7687
}
7788
}
7889

@@ -95,6 +106,41 @@ func (c *Cache) SetSessionState(sessionKey, state string) {
95106
}
96107
}
97108

109+
// GetSessionEpisode returns the cached episode for a session key
110+
func (c *Cache) GetSessionEpisode(sessionKey, ratingKey string) (*Episode, bool) {
111+
c.mu.RLock()
112+
defer c.mu.RUnlock()
113+
sessionEpisode, ok := c.sessionEpisodes[sessionKey]
114+
if !ok || sessionEpisode == nil {
115+
return nil, false
116+
}
117+
if ratingKey != "" && sessionEpisode.RatingKey != ratingKey {
118+
return nil, false
119+
}
120+
return sessionEpisode.Episode, true
121+
}
122+
123+
// SetSessionEpisode caches episode metadata for a session key
124+
func (c *Cache) SetSessionEpisode(sessionKey, ratingKey string, episode *Episode) {
125+
if sessionKey == "" || episode == nil {
126+
return
127+
}
128+
c.mu.Lock()
129+
defer c.mu.Unlock()
130+
c.sessionEpisodes[sessionKey] = &SessionEpisode{
131+
Episode: episode,
132+
RatingKey: ratingKey,
133+
CachedAt: time.Now(),
134+
}
135+
}
136+
137+
// ClearSessionEpisode removes cached episode metadata for a session key
138+
func (c *Cache) ClearSessionEpisode(sessionKey string) {
139+
c.mu.Lock()
140+
defer c.mu.Unlock()
141+
delete(c.sessionEpisodes, sessionKey)
142+
}
143+
98144
// GetDefaultStreams returns the cached default streams for an episode and user
99145
func (c *Cache) GetDefaultStreams(userID, ratingKey string) (*StreamSelection, bool) {
100146
c.mu.RLock()
@@ -296,6 +342,17 @@ func (c *Cache) CleanupExpired(maxAge time.Duration) {
296342
delete(c.lastProcessed, key)
297343
}
298344
}
345+
346+
// Clean up session episode cache
347+
for key, sessionEpisode := range c.sessionEpisodes {
348+
if sessionEpisode == nil {
349+
delete(c.sessionEpisodes, key)
350+
continue
351+
}
352+
if now.Sub(sessionEpisode.CachedAt) > maxAge {
353+
delete(c.sessionEpisodes, key)
354+
}
355+
}
299356
}
300357

301358
// Clear removes all entries from all caches
@@ -310,6 +367,7 @@ func (c *Cache) Clear() {
310367
c.newlyAdded = make(map[string]time.Time)
311368
c.recentActivities = make(map[string]time.Time)
312369
c.lastProcessed = make(map[string]time.Time)
370+
c.sessionEpisodes = make(map[string]*SessionEpisode)
313371
}
314372

315373
func defaultStreamKey(userID, ratingKey string) string {

internal/plexautolang/manager.go

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -449,18 +449,18 @@ func (m *Manager) handlePlayingNotification(targetID int64, target PlexTargetInt
449449
return
450450
}
451451
m.enqueueTask(targetID, config.MaxConcurrent, func() {
452-
m.processPlayingSession(targetID, target, cache, playing.ClientIdentifier, ratingKey, config)
452+
m.processPlayingSession(targetID, target, cache, sessionKey, playing.ClientIdentifier, ratingKey, config)
453453
})
454454
} else {
455455
// Session stopped - cleanup but also check for final track changes
456456
m.enqueueTask(targetID, config.MaxConcurrent, func() {
457-
m.processPlaybackStopped(targetID, target, playing.ClientIdentifier, ratingKey, config)
457+
m.processPlaybackStopped(targetID, target, sessionKey, playing.ClientIdentifier, ratingKey, config)
458458
})
459459
}
460460
}
461461

462462
// processPlayingSession handles an active playing session to detect track changes
463-
func (m *Manager) processPlayingSession(targetID int64, target PlexTargetInterface, cache *Cache, clientIdentifier string, ratingKey string, config *database.PlexAutoLanguagesConfig) {
463+
func (m *Manager) processPlayingSession(targetID int64, target PlexTargetInterface, cache *Cache, sessionKey string, clientIdentifier string, ratingKey string, config *database.PlexAutoLanguagesConfig) {
464464
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
465465
defer cancel()
466466

@@ -489,29 +489,28 @@ func (m *Manager) processPlayingSession(targetID int64, target PlexTargetInterfa
489489
userToken = ""
490490
}
491491

492-
// Fetch episode metadata using the user's token to see their stream preferences
493-
var episode *Episode
494-
if userToken != "" {
495-
episode, err = target.GetEpisodeWithStreamsAsUser(ctx, ratingKey, userToken)
496-
if err != nil {
497-
log.Debug().Err(err).Str("ratingKey", ratingKey).Str("user", userLabel).Msg("Plex Auto Languages: Failed to get episode as user, clearing token cache")
498-
// Token might be invalid, clear it and try again next time
499-
cache.ClearUserToken(user.ID)
500-
userToken = ""
501-
// Fall back to admin token
502-
episode, err = target.GetEpisodeWithStreams(ctx, ratingKey)
503-
if err != nil {
504-
log.Debug().Err(err).Str("ratingKey", ratingKey).Str("user", userLabel).Msg("Plex Auto Languages: Failed to get episode")
505-
return
506-
}
492+
// Prefer live session data to avoid repeated library metadata calls.
493+
episode, err := target.GetSessionEpisodeWithStreams(ctx, clientIdentifier, ratingKey)
494+
if err != nil {
495+
if errors.Is(err, ErrNoActiveSessions) {
496+
log.Trace().
497+
Str("ratingKey", ratingKey).
498+
Str("clientIdentifier", clientIdentifier).
499+
Msg("No active sessions found, falling back to cached metadata")
500+
} else {
501+
log.Trace().
502+
Err(err).
503+
Str("ratingKey", ratingKey).
504+
Str("clientIdentifier", clientIdentifier).
505+
Msg("Failed to get live session episode, falling back to cached metadata")
507506
}
508-
} else {
509-
// No user token available, use admin token
510-
episode, err = target.GetEpisodeWithStreams(ctx, ratingKey)
507+
episode, err = m.getSessionCachedEpisode(ctx, target, cache, sessionKey, ratingKey, user.ID, userLabel, &userToken)
511508
if err != nil {
512509
log.Debug().Err(err).Str("ratingKey", ratingKey).Str("user", userLabel).Msg("Plex Auto Languages: Failed to get episode")
513510
return
514511
}
512+
} else if cache != nil && sessionKey != "" {
513+
cache.SetSessionEpisode(sessionKey, ratingKey, episode)
515514
}
516515

517516
if episode.GrandparentKey == "" || len(episode.Parts) == 0 {
@@ -625,10 +624,18 @@ func (m *Manager) processPlayingSession(targetID int64, target PlexTargetInterfa
625624
}
626625

627626
// processPlaybackStopped handles when a user stops playing an episode
628-
func (m *Manager) processPlaybackStopped(targetID int64, target PlexTargetInterface, clientIdentifier string, ratingKey string, config *database.PlexAutoLanguagesConfig) {
627+
func (m *Manager) processPlaybackStopped(targetID int64, target PlexTargetInterface, sessionKey string, clientIdentifier string, ratingKey string, config *database.PlexAutoLanguagesConfig) {
629628
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
630629
defer cancel()
631630

631+
m.mu.RLock()
632+
cache := m.caches[targetID]
633+
m.mu.RUnlock()
634+
635+
if cache != nil && sessionKey != "" {
636+
defer cache.ClearSessionEpisode(sessionKey)
637+
}
638+
632639
// Get the episode with streams to see what was selected (prefer live session data)
633640
liveData := true
634641
episode, err := target.GetSessionEpisodeWithStreams(ctx, clientIdentifier, ratingKey)
@@ -675,11 +682,6 @@ func (m *Manager) processPlaybackStopped(targetID int64, target PlexTargetInterf
675682
// If live data is gone (session already stopped), try to reapply the last known stream
676683
// selection from cache so we don't lose mid-playback changes.
677684
if !liveData {
678-
var cache *Cache
679-
m.mu.RLock()
680-
cache = m.caches[targetID]
681-
m.mu.RUnlock()
682-
683685
if cache != nil {
684686
if cachedStreams, ok := cache.GetDefaultStreams(user.ID, ratingKey); ok {
685687
part := &episode.Parts[0]
@@ -746,10 +748,6 @@ func (m *Manager) processPlaybackStopped(targetID int64, target PlexTargetInterf
746748
Msg("Plex Auto Languages: User track preference saved")
747749

748750
// Apply preference to other episodes
749-
m.mu.RLock()
750-
cache := m.caches[targetID]
751-
m.mu.RUnlock()
752-
753751
userToken, err := m.getUserToken(ctx, cache, target, user.ID)
754752
if err != nil {
755753
log.Debug().Err(err).Str("user", userLabel).Msg("Plex Auto Languages: Failed to get user token, skipping apply")
@@ -1352,6 +1350,52 @@ func (m *Manager) getUserToken(ctx context.Context, cache *Cache, target PlexTar
13521350
return token, nil
13531351
}
13541352

1353+
func (m *Manager) getSessionCachedEpisode(
1354+
ctx context.Context,
1355+
target PlexTargetInterface,
1356+
cache *Cache,
1357+
sessionKey string,
1358+
ratingKey string,
1359+
userID string,
1360+
userLabel string,
1361+
userToken *string,
1362+
) (*Episode, error) {
1363+
if cache != nil {
1364+
if cached, ok := cache.GetSessionEpisode(sessionKey, ratingKey); ok {
1365+
return cached, nil
1366+
}
1367+
}
1368+
1369+
if userToken != nil && *userToken != "" {
1370+
episode, err := target.GetEpisodeWithStreamsAsUser(ctx, ratingKey, *userToken)
1371+
if err == nil {
1372+
if cache != nil && sessionKey != "" {
1373+
cache.SetSessionEpisode(sessionKey, ratingKey, episode)
1374+
}
1375+
return episode, nil
1376+
}
1377+
1378+
log.Debug().
1379+
Err(err).
1380+
Str("ratingKey", ratingKey).
1381+
Str("user", userLabel).
1382+
Msg("Plex Auto Languages: Failed to get episode as user, clearing token cache")
1383+
if cache != nil {
1384+
cache.ClearUserToken(userID)
1385+
}
1386+
*userToken = ""
1387+
}
1388+
1389+
episode, err := target.GetEpisodeWithStreams(ctx, ratingKey)
1390+
if err != nil {
1391+
return nil, err
1392+
}
1393+
if cache != nil && sessionKey != "" {
1394+
cache.SetSessionEpisode(sessionKey, ratingKey, episode)
1395+
}
1396+
return episode, nil
1397+
}
1398+
13551399
// tracksMatchPreference checks if selected tracks match the stored preference
13561400
func (m *Manager) tracksMatchPreference(audio *AudioStream, subtitle *SubtitleStream, pref *database.PlexAutoLanguagesPreference) bool {
13571401
if pref == nil {

0 commit comments

Comments
 (0)