Files
nilsandClaude Opus 5.5 f0bc284e82 Trakt lists: keep the Radarr/Sonarr lookups of their titles on disk
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>
2026-10-24 20:48:20 +02:00

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)
}