153 lines
3.2 KiB
Go
153 lines
3.2 KiB
Go
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
|
|
}
|