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:
@@ -2,12 +2,14 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/sonarr"
|
||||
)
|
||||
|
||||
// movieLabel is the one-line list entry for a movie.
|
||||
@@ -81,3 +83,69 @@ func folderIndex(folders []arr.RootFolder, path string) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func itoa(n int) string { return strconv.Itoa(n) }
|
||||
|
||||
// seriesLabel is the one-line list entry for a series.
|
||||
func seriesLabel(s sonarr.Series) string {
|
||||
label := tview.Escape(s.Title)
|
||||
if s.Year > 0 {
|
||||
label += fmt.Sprintf(" (%d)", s.Year)
|
||||
}
|
||||
if s.InLibrary() {
|
||||
label += " [green]✓[-]"
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
// seriesDetails renders the detail pane text for a series.
|
||||
func seriesDetails(s sonarr.Series) string {
|
||||
var b strings.Builder
|
||||
title := tview.Escape(s.Title)
|
||||
if s.Year > 0 {
|
||||
title += fmt.Sprintf(" (%d)", s.Year)
|
||||
}
|
||||
fmt.Fprintf(&b, "[::b]%s[::-]\n", title)
|
||||
|
||||
var facts []string
|
||||
if s.Ratings.Value > 0 {
|
||||
facts = append(facts, fmt.Sprintf("★ %.1f", s.Ratings.Value))
|
||||
}
|
||||
if n := s.SeasonCount(); n > 0 {
|
||||
facts = append(facts, fmt.Sprintf("%d season%s", n, plural(n)))
|
||||
}
|
||||
if s.Runtime > 0 {
|
||||
facts = append(facts, fmt.Sprintf("%d min", s.Runtime))
|
||||
}
|
||||
if len(s.Genres) > 0 {
|
||||
facts = append(facts, tview.Escape(strings.Join(s.Genres, ", ")))
|
||||
}
|
||||
if len(facts) > 0 {
|
||||
b.WriteString(strings.Join(facts, " · ") + "\n")
|
||||
}
|
||||
if s.Network != "" {
|
||||
fmt.Fprintf(&b, "Network: %s\n", tview.Escape(s.Network))
|
||||
}
|
||||
if s.Status != "" {
|
||||
fmt.Fprintf(&b, "Status: %s\n", tview.Escape(s.Status))
|
||||
}
|
||||
if s.InLibrary() {
|
||||
st := s.Statistics
|
||||
if st.EpisodeCount > 0 && st.EpisodeFileCount >= st.EpisodeCount {
|
||||
b.WriteString("[green]In library, all episodes downloaded[-]\n")
|
||||
} else {
|
||||
fmt.Fprintf(&b, "[yellow]In library, %d/%d episodes downloaded[-]\n", st.EpisodeFileCount, st.EpisodeCount)
|
||||
}
|
||||
}
|
||||
if s.Overview != "" {
|
||||
b.WriteString("\n" + tview.Escape(s.Overview) + "\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func plural(n int) string {
|
||||
if n == 1 {
|
||||
return ""
|
||||
}
|
||||
return "s"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user