package ui import ( "context" "errors" "time" "git.stinnesbeck.com/nils/arr-tui/internal/trakt" ) const ( maxPeopleCache = 500 peopleRetryWait = 2 * time.Minute // a failed lookup is not repeated sooner ) // peopleCache holds the casts loaded from Trakt and the lookups in flight. // UI goroutine only. type peopleCache struct { done map[string]trakt.People failed map[string]time.Time waiting map[string][]func(trakt.People, error) } func newPeopleCache() *peopleCache { return &peopleCache{done: map[string]trakt.People{}, failed: map[string]time.Time{}, waiting: map[string][]func(trakt.People, error){}} } // loadPeople delivers the cast of a title to done on the UI goroutine, // asking Trakt in the background when it is not cached. A title Trakt does // not know is remembered as having no cast. func (a *App) loadPeople(r trakt.Ref, done func(trakt.People, error)) { c, key := a.people, r.Key() if p, ok := c.done[key]; ok { done(p, nil) return } if t, ok := c.failed[key]; ok && time.Since(t) < peopleRetryWait { done(trakt.People{}, errors.New("cast not available")) return } if _, inflight := c.waiting[key]; inflight { c.waiting[key] = append(c.waiting[key], done) return } c.waiting[key] = []func(trakt.People, error){done} go func() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() p, err := a.trakt.People(ctx, r) if errors.Is(err, trakt.ErrNotFound) { p, err = trakt.People{}, nil } a.post(func() { if a.people != c { return // dropped meanwhile (the client ID changed) } waiters := c.waiting[key] delete(c.waiting, key) if err != nil { c.failed[key] = time.Now() } else { if len(c.done) >= maxPeopleCache { c.done = map[string]trakt.People{} } c.done[key] = p } for _, w := range waiters { w(p, err) } }) }() } // showPeople loads the cast of the title the pane shows, once the selection // rests (like the poster: holding an arrow key must not send a request per // step, Trakt allows 500 per five minutes). func (d *detailPane) showPeople(r trakt.Ref) { key := r.Key() if key == d.castKey { return // the same title again (a refresh): keep what is shown } if d.castSettle != nil { d.castSettle.Stop() } d.castKey, d.cast = key, "" if key == "" || !d.app.trakt.Configured() { return } now := time.Now() moving := now.Sub(d.castShown) < posterSettle d.castShown = now load := func() { d.app.loadPeople(r, func(p trakt.People, err error) { if d.castKey != key || err != nil { return } d.cast = peopleText(p, r.Kind) d.laidOut.valid = false }) } if moving { d.castSettle = time.AfterFunc(posterSettle, func() { d.app.post(func() { if d.castKey == key { load() } }) }) return } load() }