Add Sonarr client and series search/add flow (M3)

Sonarr client with lookup, add series (monitor modes) and search.
Refactor the movie view into a generic search view driven by a backend
interface so Movies and Series share one implementation.
Add F1-F4 / Alt+1-4 view switching that works while typing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 17:49:11 +02:00
co-authored by Claude Sonnet 5.5
parent 51fb0556b0
commit 2dd7c17737
8 changed files with 631 additions and 68 deletions
+43
View File
@@ -6,6 +6,7 @@ import (
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
"git.stinnesbeck.com/nils/arr-tui/internal/sonarr"
)
func TestMovieLabel(t *testing.T) {
@@ -43,3 +44,45 @@ func TestDefaultIndexes(t *testing.T) {
t.Error("folderIndex")
}
}
func TestSeriesLabelAndDetails(t *testing.T) {
s := sonarr.Series{
Title: "Breaking Bad", Year: 2008, Network: "AMC", Status: "ended", Runtime: 47,
Genres: []string{"Crime"},
Ratings: sonarr.Rating{Value: 9.3},
Seasons: []sonarr.Season{{SeasonNumber: 0}, {SeasonNumber: 1}},
}
if got := seriesLabel(s); got != "Breaking Bad (2008)" {
t.Errorf("label = %q", got)
}
got := seriesDetails(s)
for _, want := range []string{"Breaking Bad (2008)", "★ 9.3", "1 season ·", "47 min", "Network: AMC", "Status: ended"} {
if !strings.Contains(got, want) {
t.Errorf("details missing %q:\n%s", want, got)
}
}
s.ID = 1
s.Statistics = sonarr.Statistics{EpisodeCount: 10, EpisodeFileCount: 4}
if got := seriesDetails(s); !strings.Contains(got, "4/10 episodes") {
t.Errorf("progress missing:\n%s", got)
}
s.Statistics.EpisodeFileCount = 10
if got := seriesDetails(s); !strings.Contains(got, "all episodes downloaded") {
t.Errorf("complete marker missing:\n%s", got)
}
}
func TestResultKeysAndWithID(t *testing.T) {
var r result = movieResult{radarr.Movie{TMDBID: 603, Title: "x"}}
if r.key() != "tmdb:603" || r.inLibrary() {
t.Fatalf("movie key/inLibrary: %q %v", r.key(), r.inLibrary())
}
if added := r.withID(5); !added.inLibrary() || r.inLibrary() {
t.Error("withID must return a flagged copy and leave the original alone")
}
r = seriesResult{sonarr.Series{TVDBID: 81189}}
if r.key() != "tvdb:81189" {
t.Errorf("series key = %q", r.key())
}
}