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
+27 -3
View File
@@ -8,6 +8,8 @@ import (
"github.com/rivo/tview"
"git.stinnesbeck.com/nils/arr-tui/internal/config"
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
"git.stinnesbeck.com/nils/arr-tui/internal/sonarr"
)
const modalPage = "modal"
@@ -42,8 +44,8 @@ func New(cfg *config.Config) *App {
}
a.views = []view{
{'1', "Movies", newMoviesView(a, cfg.Radarr).root},
{'2', "Series", placeholder("Series search (M3)")},
{'1', "Movies", newSearchView(a, radarrBackend{radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)}, cfg.Radarr).root},
{'2', "Series", newSearchView(a, sonarrBackend{sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)}, cfg.Sonarr).root},
{'3', "Downloads", placeholder("Downloads (M4/M5)")},
{'4', "Library", placeholder("Library (v0.2)")},
}
@@ -81,7 +83,7 @@ func (a *App) Run() error {
}
func (a *App) renderStatus() {
hints := "[gray]q quit · Tab/1-4 views"
hints := "[gray]q quit · Tab/1-4/F1-F4 views · / search"
if a.message == "" {
a.status.SetText(hints)
return
@@ -128,6 +130,13 @@ func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey {
a.app.Stop()
return nil
}
// View switching that also works while typing in a search box. Not while a dialog is open.
if !a.modalOpen {
if i := a.viewShortcut(ev); i >= 0 && i < len(a.views) {
a.switchTo(i)
return nil
}
}
if a.typing() {
return ev
}
@@ -153,6 +162,21 @@ func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey {
return ev
}
// viewShortcut returns the view index for F1-F4 or Alt+1-4, or -1.
func (a *App) viewShortcut(ev *tcell.EventKey) int {
if ev.Key() >= tcell.KeyF1 && ev.Key() <= tcell.KeyF12 {
return int(ev.Key() - tcell.KeyF1)
}
if ev.Key() == tcell.KeyRune && ev.Modifiers()&tcell.ModAlt != 0 {
for i, v := range a.views {
if ev.Rune() == v.key {
return i
}
}
}
return -1
}
func (a *App) switchTo(i int) {
a.active = i
a.pages.SwitchToPage(a.views[i].name)