Details pane: director and cast from Trakt
With a Trakt client ID, the details of a movie or series show the director(s) or creators and up to 8 cast members with their roles below the plot, in every view. The title is found on Trakt by its TMDb/TVDB (else IMDb) id, then its people are read. Casts are cached and, like posters, only loaded once the selection rests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user