Files
arr-tui/internal/ui/format.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

152 lines
3.7 KiB
Go

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.
func movieLabel(m radarr.Movie) string {
label := tview.Escape(m.Title)
if m.Year > 0 {
label += fmt.Sprintf(" (%d)", m.Year)
}
if m.InLibrary() {
label += " [green]✓[-]"
}
return label
}
// movieDetails renders the detail pane text for a movie.
func movieDetails(m radarr.Movie) string {
var b strings.Builder
title := tview.Escape(m.Title)
if m.Year > 0 {
title += fmt.Sprintf(" (%d)", m.Year)
}
fmt.Fprintf(&b, "[::b]%s[::-]\n", title)
var facts []string
if r := m.Rating(); r > 0 {
facts = append(facts, fmt.Sprintf("★ %.1f", r))
}
if m.Runtime > 0 {
facts = append(facts, fmt.Sprintf("%dh%02dm", m.Runtime/60, m.Runtime%60))
}
if len(m.Genres) > 0 {
facts = append(facts, tview.Escape(strings.Join(m.Genres, ", ")))
}
if len(facts) > 0 {
b.WriteString(strings.Join(facts, " · ") + "\n")
}
if m.Studio != "" {
fmt.Fprintf(&b, "Studio: %s\n", tview.Escape(m.Studio))
}
if m.Status != "" {
fmt.Fprintf(&b, "Status: %s\n", tview.Escape(m.Status))
}
switch {
case m.HasFile:
b.WriteString("[green]In library, downloaded[-]\n")
case m.InLibrary():
b.WriteString("[yellow]In library, not downloaded[-]\n")
}
if m.Overview != "" {
b.WriteString("\n" + tview.Escape(m.Overview) + "\n")
}
return b.String()
}
// profileIndex returns the index of the profile named name, or 0.
func profileIndex(profiles []arr.QualityProfile, name string) int {
for i, p := range profiles {
if strings.EqualFold(p.Name, name) {
return i
}
}
return 0
}
// folderIndex returns the index of the root folder at path, or 0.
func folderIndex(folders []arr.RootFolder, path string) int {
for i, f := range folders {
if strings.TrimRight(f.Path, "/") == strings.TrimRight(path, "/") {
return i
}
}
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"
}