feat: add source cache with stale-while-revalidate
This commit is contained in:
152
internal/playback/source_cache.go
Normal file
152
internal/playback/source_cache.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mal/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSourceCacheTTL = 5 * time.Minute
|
||||
defaultSourceCacheStaleTTL = 10 * time.Minute
|
||||
defaultSourceCacheMaxEntries = 512
|
||||
)
|
||||
|
||||
type sourceCacheKey struct {
|
||||
animeID int
|
||||
episode string
|
||||
mode string
|
||||
}
|
||||
|
||||
func newSourceCacheKey(animeID int, episode string, mode string) sourceCacheKey {
|
||||
return sourceCacheKey{
|
||||
animeID: animeID,
|
||||
episode: strings.TrimSpace(episode),
|
||||
mode: normalizeSourceMode(mode),
|
||||
}
|
||||
}
|
||||
|
||||
func (k sourceCacheKey) flightKey() string {
|
||||
return fmt.Sprintf("%d|%q|%q", k.animeID, k.episode, k.mode)
|
||||
}
|
||||
|
||||
func normalizeSourceMode(mode string) string {
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
if mode == "" {
|
||||
return "sub"
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
type sourceCacheEntry struct {
|
||||
key sourceCacheKey
|
||||
result *domain.StreamResult
|
||||
freshUntil time.Time
|
||||
staleUntil time.Time
|
||||
}
|
||||
|
||||
type sourceCacheState uint8
|
||||
|
||||
const (
|
||||
sourceCacheMiss sourceCacheState = iota
|
||||
sourceCacheFresh
|
||||
sourceCacheStale
|
||||
)
|
||||
|
||||
type sourceCache struct {
|
||||
mu sync.Mutex
|
||||
freshTTL time.Duration
|
||||
staleTTL time.Duration
|
||||
maxEntries int
|
||||
entries map[sourceCacheKey]*list.Element
|
||||
lru *list.List
|
||||
}
|
||||
|
||||
func newSourceCache(freshTTL, staleTTL time.Duration, maxEntries int) *sourceCache {
|
||||
if freshTTL <= 0 {
|
||||
freshTTL = defaultSourceCacheTTL
|
||||
}
|
||||
if staleTTL < 0 {
|
||||
staleTTL = 0
|
||||
}
|
||||
if maxEntries <= 0 {
|
||||
maxEntries = defaultSourceCacheMaxEntries
|
||||
}
|
||||
return &sourceCache{
|
||||
freshTTL: freshTTL,
|
||||
staleTTL: staleTTL,
|
||||
maxEntries: maxEntries,
|
||||
entries: make(map[sourceCacheKey]*list.Element, maxEntries),
|
||||
lru: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sourceCache) get(key sourceCacheKey, now time.Time) (*domain.StreamResult, sourceCacheState) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
el := c.entries[key]
|
||||
if el == nil {
|
||||
return nil, sourceCacheMiss
|
||||
}
|
||||
entry, ok := el.Value.(sourceCacheEntry)
|
||||
if !ok || !now.Before(entry.staleUntil) {
|
||||
c.remove(el)
|
||||
return nil, sourceCacheMiss
|
||||
}
|
||||
c.lru.MoveToFront(el)
|
||||
if now.Before(entry.freshUntil) {
|
||||
return cloneStreamResult(entry.result), sourceCacheFresh
|
||||
}
|
||||
return cloneStreamResult(entry.result), sourceCacheStale
|
||||
}
|
||||
|
||||
func (c *sourceCache) set(key sourceCacheKey, result *domain.StreamResult, now time.Time) (evicted bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entry := sourceCacheEntry{
|
||||
key: key,
|
||||
result: cloneStreamResult(result),
|
||||
freshUntil: now.Add(c.freshTTL),
|
||||
staleUntil: now.Add(c.freshTTL + c.staleTTL),
|
||||
}
|
||||
if el := c.entries[key]; el != nil {
|
||||
el.Value = entry
|
||||
c.lru.MoveToFront(el)
|
||||
return false
|
||||
}
|
||||
|
||||
c.entries[key] = c.lru.PushFront(entry)
|
||||
for len(c.entries) > c.maxEntries {
|
||||
back := c.lru.Back()
|
||||
if back == nil {
|
||||
break
|
||||
}
|
||||
c.remove(back)
|
||||
evicted = true
|
||||
}
|
||||
return evicted
|
||||
}
|
||||
|
||||
func (c *sourceCache) remove(el *list.Element) {
|
||||
entry, ok := el.Value.(sourceCacheEntry)
|
||||
if ok {
|
||||
delete(c.entries, entry.key)
|
||||
}
|
||||
c.lru.Remove(el)
|
||||
}
|
||||
|
||||
func cloneStreamResult(result *domain.StreamResult) *domain.StreamResult {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *result
|
||||
cloned.Subtitles = append([]domain.Subtitle(nil), result.Subtitles...)
|
||||
cloned.Qualities = append([]domain.StreamSource(nil), result.Qualities...)
|
||||
return &cloned
|
||||
}
|
||||
Reference in New Issue
Block a user