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