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>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user