Files
arr-tui/internal/ui/backend.go
T
nilsandClaude Sonnet 5.5 2dd7c17737 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>
2026-10-20 17:49:11 +02:00

136 lines
4.0 KiB
Go

package ui
import (
"context"
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
"git.stinnesbeck.com/nils/arr-tui/internal/sonarr"
)
// result is one search hit, independent of which *arr produced it.
type result interface {
key() string // stable identity, e.g. "tmdb:603"
title() string
label() string
details() string
inLibrary() bool
withID(id int) result // copy flagged as added to the library
}
// monitorMode is a Sonarr-style "which episodes to monitor" choice.
type monitorMode struct {
label string
value string
}
// addChoice is what the add dialog collects.
type addChoice struct {
profileID int
folder string
monitored bool
searchNow bool
monitor string // empty for backends without monitor modes
}
// backend adapts Radarr or Sonarr to the generic search view.
type backend interface {
name() string // "Radarr"
noun() string // "Movies"
arr() *arr.Client
search(ctx context.Context, term string) ([]result, error)
monitorModes() []monitorMode // nil if not applicable
add(ctx context.Context, r result, c addChoice) (id int, err error)
}
// --- Radarr ---
type movieResult struct{ radarr.Movie }
func (m movieResult) key() string { return radarr.TMDBTerm(m.TMDBID) }
func (m movieResult) title() string { return m.Title }
func (m movieResult) label() string { return movieLabel(m.Movie) }
func (m movieResult) details() string { return movieDetails(m.Movie) }
func (m movieResult) inLibrary() bool { return m.InLibrary() }
func (m movieResult) withID(id int) result {
m.ID = id
return m
}
type radarrBackend struct{ c *radarr.Client }
func (b radarrBackend) name() string { return "Radarr" }
func (b radarrBackend) noun() string { return "Movies" }
func (b radarrBackend) arr() *arr.Client { return b.c.Client }
func (radarrBackend) monitorModes() []monitorMode { return nil }
func (b radarrBackend) search(ctx context.Context, term string) ([]result, error) {
movies, err := b.c.Lookup(ctx, term)
out := make([]result, len(movies))
for i, m := range movies {
out[i] = movieResult{m}
}
return out, err
}
func (b radarrBackend) add(ctx context.Context, r result, c addChoice) (int, error) {
created, err := b.c.AddMovie(ctx, r.(movieResult).Movie, radarr.AddOptions{
QualityProfileID: c.profileID,
RootFolderPath: c.folder,
Monitored: c.monitored,
SearchNow: c.searchNow,
})
return created.ID, err
}
// --- Sonarr ---
type seriesResult struct{ sonarr.Series }
func (s seriesResult) key() string { return "tvdb:" + itoa(s.TVDBID) }
func (s seriesResult) title() string { return s.Title }
func (s seriesResult) label() string { return seriesLabel(s.Series) }
func (s seriesResult) details() string { return seriesDetails(s.Series) }
func (s seriesResult) inLibrary() bool { return s.InLibrary() }
func (s seriesResult) withID(id int) result {
s.ID = id
return s
}
type sonarrBackend struct{ c *sonarr.Client }
func (b sonarrBackend) name() string { return "Sonarr" }
func (b sonarrBackend) noun() string { return "Series" }
func (b sonarrBackend) arr() *arr.Client { return b.c.Client }
func (sonarrBackend) monitorModes() []monitorMode {
return []monitorMode{
{"All episodes", sonarr.MonitorAll},
{"Future episodes", sonarr.MonitorFuture},
{"Missing episodes", sonarr.MonitorMissing},
{"First season", sonarr.MonitorFirstSeason},
{"Latest season", sonarr.MonitorLatestSeason},
{"None", sonarr.MonitorNone},
}
}
func (b sonarrBackend) search(ctx context.Context, term string) ([]result, error) {
series, err := b.c.Lookup(ctx, term)
out := make([]result, len(series))
for i, s := range series {
out[i] = seriesResult{s}
}
return out, err
}
func (b sonarrBackend) add(ctx context.Context, r result, c addChoice) (int, error) {
created, err := b.c.AddSeries(ctx, r.(seriesResult).Series, sonarr.AddOptions{
QualityProfileID: c.profileID,
RootFolderPath: c.folder,
Monitored: c.monitored,
Monitor: c.monitor,
SearchNow: c.searchNow,
})
return created.ID, err
}