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:
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user