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:
2026-10-23 23:31:33 +02:00
co-authored by Claude Opus 5.5
parent 8775c27aab
commit 12e825cff0
13 changed files with 468 additions and 17 deletions
+59
View File
@@ -9,6 +9,8 @@ import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"git.stinnesbeck.com/nils/arr-tui/internal/trakt"
)
func rowText(screen tcell.SimulationScreen, y, x0, x1 int) string {
@@ -137,3 +139,60 @@ func TestPosterWaitsWhileTheSelectionMoves(t *testing.T) {
t.Error("a selection at rest should show its poster")
}
}
func TestCastBelowThePlot(t *testing.T) {
a := newTestApp(t)
d := newDetailPane(a)
dune := trakt.Ref{Kind: trakt.Movies, TMDB: 438631}
heat := trakt.Ref{Kind: trakt.Movies, TMDB: 949}
a.people.done[dune.Key()] = trakt.People{
Leaders: []trakt.Person{{Name: "Denis Villeneuve", Role: "Director"}},
Cast: []trakt.Person{{Name: "Timothée Chalamet", Role: "Paul Atreides"}, {Name: "Rebecca Ferguson", Role: "Lady Jessica"}},
}
a.people.done[heat.Key()] = trakt.People{}
// Without a Trakt client ID nothing is asked or shown.
d.Show(detailText{head: "Dune", people: dune}, coverRef{})
if d.cast != "" {
t.Fatalf("cast without Trakt: %q", d.cast)
}
a.trakt.Reconfigure("id")
d.castKey, d.castShown = "", time.Time{}
d.Show(detailText{head: "Dune", plot: "Spice.", people: dune}, coverRef{})
if !strings.Contains(d.cast, "Director[::-] Denis Villeneuve") || !strings.Contains(d.cast, "Timothée Chalamet [gray]as Paul Atreides") {
t.Errorf("cast %q", d.cast)
}
screen := tcell.NewSimulationScreen("")
if err := screen.Init(); err != nil {
t.Fatal(err)
}
defer screen.Fini()
screen.SetSize(60, 20)
d.SetRect(0, 0, 60, 20)
d.Draw(screen)
if got := d.text.GetText(true); !strings.Contains(got, "Spice.\n\nDirector Denis Villeneuve\nCast\nTimothée Chalamet as Paul Atreides") {
t.Errorf("pane text %q", got)
}
// A quick move to the next title waits with its cast until the selection rests.
d.Show(detailText{head: "Heat", people: heat}, coverRef{})
if d.cast != "" || d.castSettle == nil {
t.Errorf("moving: cast %q", d.cast)
}
d.castSettle.Stop()
}
func TestPeopleText(t *testing.T) {
var cast []trakt.Person
for i := 0; i < 10; i++ {
cast = append(cast, trakt.Person{Name: "Actor " + string(rune('A'+i))})
}
got := peopleText(trakt.People{Cast: cast, Leaders: []trakt.Person{{Name: "Vince [Gilligan]"}}}, trakt.Shows)
if !strings.HasPrefix(got, "[::b]Created by[::-] Vince [Gilligan[]") || !strings.HasSuffix(got, "[gray]and 2 more[-]") || strings.Contains(got, "Actor I") {
t.Errorf("text %q", got)
}
if peopleText(trakt.People{}, trakt.Movies) != "" {
t.Error("no people, no text")
}
}