Library membership is asked every time via movie?tmdbId= / series?tvdbId=; only the lookups of titles not in the library are stored (3 days). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
package ui
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
lookupTTL = 3 * 24 * time.Hour // a stored lookup is used this long
|
|
maxLookupsSaved = 2000 // lookups kept (in memory and on disk)
|
|
lookupSaveDelay = 5 * time.Second // new lookups are written to disk this long after the last one
|
|
)
|
|
|
|
// lookupStore keeps the Radarr/Sonarr lookups of Trakt list titles that are
|
|
// not in the library, so a list shows at once after a restart: a lookup asks
|
|
// the metadata provider and takes about a second, while whether a title is
|
|
// in the library is asked of the server every time (that is quick, and must
|
|
// not be stale). Safe for use from any goroutine; read from disk on first use.
|
|
type lookupStore struct {
|
|
disk *diskCache
|
|
|
|
mu sync.Mutex
|
|
loaded bool
|
|
entries map[string]savedLookup
|
|
dirty bool
|
|
timer *time.Timer
|
|
}
|
|
|
|
// savedLookup is one lookup result as Radarr/Sonarr sent it.
|
|
type savedLookup struct {
|
|
Saved time.Time `json:"saved"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
func newLookupStore(disk *diskCache) *lookupStore { return &lookupStore{disk: disk} }
|
|
|
|
func (s *lookupStore) load() {
|
|
if !s.loaded {
|
|
s.loaded, s.entries = true, s.disk.loadLookups()
|
|
}
|
|
}
|
|
|
|
// get decodes the stored lookup of key into v, if there is a fresh one.
|
|
func (s *lookupStore) get(key string, v any) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.load()
|
|
e, ok := s.entries[key]
|
|
if !ok || time.Since(e.Saved) > lookupTTL {
|
|
return false
|
|
}
|
|
return json.Unmarshal(e.Data, v) == nil
|
|
}
|
|
|
|
// put stores a lookup, dropping the oldest ones beyond maxLookupsSaved, and
|
|
// has it written to disk soon.
|
|
func (s *lookupStore) put(key string, v any) {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.load()
|
|
s.entries[key] = savedLookup{Saved: time.Now(), Data: data}
|
|
if len(s.entries) > maxLookupsSaved {
|
|
keys := slices.SortedFunc(maps.Keys(s.entries), func(x, y string) int {
|
|
return s.entries[x].Saved.Compare(s.entries[y].Saved)
|
|
})
|
|
for _, k := range keys[:len(keys)-maxLookupsSaved] {
|
|
delete(s.entries, k)
|
|
}
|
|
}
|
|
s.dirty = true
|
|
if s.timer != nil {
|
|
s.timer.Stop()
|
|
}
|
|
s.timer = time.AfterFunc(lookupSaveDelay, func() { _ = s.save() })
|
|
}
|
|
|
|
// save writes the lookups to disk if there are new ones.
|
|
func (s *lookupStore) save() error {
|
|
s.mu.Lock()
|
|
if !s.dirty {
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
s.dirty = false
|
|
all := maps.Clone(s.entries)
|
|
s.mu.Unlock()
|
|
return s.disk.saveLookups(all)
|
|
}
|