Config: pick the default quality profile and root folder from the server
The two defaults of Radarr and Sonarr are dropdowns filled from the server with the address and key in the fields: when the view opens, on Revert, and shortly after either is changed. A configured value the server does not list stays selectable, marked "(not on the server)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
|
||||
)
|
||||
|
||||
// serverChoice is a setting whose values come from the server: a dropdown of
|
||||
// the names Radarr/Sonarr know (quality profiles, root folders), with "(none)"
|
||||
// first. The value in use is kept as an option even when the server does not
|
||||
// list it (or cannot be asked), so nothing is lost by opening the view.
|
||||
type serverChoice struct {
|
||||
*tview.DropDown
|
||||
value string // the selected value; "" = none
|
||||
names []string // from the server, in its order
|
||||
loaded bool // names came from the server
|
||||
quiet func() func()
|
||||
picked func(text string, index int) // SetOptions replaces it, so it is kept here
|
||||
}
|
||||
|
||||
const noChoice = "(none)"
|
||||
|
||||
// newServerChoice makes the dropdown. quiet marks changes as not made by the
|
||||
// user while the options are rebuilt; it returns the function that ends that.
|
||||
func newServerChoice(label string, quiet func() func(), changed func()) *serverChoice {
|
||||
c := &serverChoice{DropDown: tview.NewDropDown().SetLabel(label), quiet: quiet}
|
||||
c.picked = func(_ string, i int) {
|
||||
c.value = c.valueAt(i)
|
||||
changed()
|
||||
}
|
||||
c.rebuild()
|
||||
return c
|
||||
}
|
||||
|
||||
// options lists the values in the order shown: none, the server's names, and
|
||||
// the value in use when the server does not have it.
|
||||
func (c *serverChoice) options() []string {
|
||||
opts := append([]string{""}, c.names...)
|
||||
if c.value != "" && !contains(c.names, c.value) {
|
||||
opts = append(opts, c.value)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func (c *serverChoice) valueAt(i int) string {
|
||||
opts := c.options()
|
||||
if i < 0 || i >= len(opts) {
|
||||
return ""
|
||||
}
|
||||
return opts[i]
|
||||
}
|
||||
|
||||
// rebuild puts the options into the dropdown and selects the value.
|
||||
func (c *serverChoice) rebuild() {
|
||||
done := c.quiet()
|
||||
defer done()
|
||||
value := c.value
|
||||
var labels []string
|
||||
sel := 0
|
||||
for i, o := range c.options() {
|
||||
label := o
|
||||
switch {
|
||||
case o == "":
|
||||
label = noChoice
|
||||
case c.loaded && !contains(c.names, o):
|
||||
label = o + " (not on the server)"
|
||||
}
|
||||
labels = append(labels, label)
|
||||
if o == value {
|
||||
sel = i
|
||||
}
|
||||
}
|
||||
c.value = value
|
||||
c.SetOptions(labels, c.picked)
|
||||
c.SetCurrentOption(sel) // reports sel, which is value
|
||||
}
|
||||
|
||||
// set selects a value.
|
||||
func (c *serverChoice) set(value string) {
|
||||
c.value = strings.TrimSpace(value)
|
||||
c.rebuild()
|
||||
}
|
||||
|
||||
// setNames replaces the server's names; nil with loaded false means they are
|
||||
// not known.
|
||||
func (c *serverChoice) setNames(names []string, loaded bool) {
|
||||
c.names, c.loaded = names, loaded
|
||||
c.rebuild()
|
||||
}
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// choicesDelay is how long typing in an address or API key pauses before the
|
||||
// server is asked for its profiles and folders.
|
||||
const choicesDelay = 700 * time.Millisecond
|
||||
|
||||
// scheduleChoices asks the service again once its address and key have not
|
||||
// changed for choicesDelay.
|
||||
func (v *settingsView) scheduleChoices(service string) {
|
||||
if t := v.choiceTimers[service]; t != nil {
|
||||
t.Stop()
|
||||
}
|
||||
v.choiceTimers[service] = time.AfterFunc(choicesDelay, func() {
|
||||
v.app.post(func() { v.loadChoices(service) })
|
||||
})
|
||||
}
|
||||
|
||||
// loadChoices fills the profile and folder dropdowns of a service ("radarr" or
|
||||
// "sonarr") from the server, using the address and key typed in the fields.
|
||||
func (v *settingsView) loadChoices(service string) {
|
||||
profile, folder := v.choices[service+".profile"], v.choices[service+".folder"]
|
||||
url := strings.TrimSpace(v.inputs[service+".url"].GetText())
|
||||
key := strings.TrimSpace(v.inputs[service+".apikey"].GetText())
|
||||
v.choiceGen[service]++
|
||||
gen := v.choiceGen[service]
|
||||
if url == "" || key == "" {
|
||||
profile.setNames(nil, false)
|
||||
folder.setNames(nil, false)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
profiles, folders, err := fetchChoices(ctx, url, key)
|
||||
v.app.post(func() { v.applyChoices(service, gen, profiles, folders, err) })
|
||||
}()
|
||||
}
|
||||
|
||||
// fetchChoices asks a Radarr/Sonarr for its quality profile names and root
|
||||
// folder paths.
|
||||
func fetchChoices(ctx context.Context, url, key string) (profiles, folders []string, err error) {
|
||||
client := arr.NewClient(url, key)
|
||||
qp, err := client.QualityProfiles(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rf, err := client.RootFolders(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, p := range qp {
|
||||
profiles = append(profiles, p.Name)
|
||||
}
|
||||
for _, f := range rf {
|
||||
folders = append(folders, f.Path)
|
||||
}
|
||||
return profiles, folders, nil
|
||||
}
|
||||
|
||||
// applyChoices puts what fetchChoices found into the dropdowns, unless the
|
||||
// address or key changed since it was asked (gen).
|
||||
func (v *settingsView) applyChoices(service string, gen int, profiles, folders []string, err error) {
|
||||
if v.choiceGen[service] != gen {
|
||||
return
|
||||
}
|
||||
profile, folder := v.choices[service+".profile"], v.choices[service+".folder"]
|
||||
if err != nil {
|
||||
profile.setNames(nil, false)
|
||||
folder.setNames(nil, false)
|
||||
name := map[string]string{"radarr": "Radarr", "sonarr": "Sonarr"}[service]
|
||||
if msg := err.Error(); v.choiceErr[service] != msg {
|
||||
v.choiceErr[service] = msg
|
||||
v.report("[yellow]" + name + ": quality profiles and root folders not loaded: " + tview.Escape(msg) + "[-]")
|
||||
}
|
||||
return
|
||||
}
|
||||
delete(v.choiceErr, service)
|
||||
profile.setNames(profiles, true)
|
||||
folder.setNames(folders, true)
|
||||
}
|
||||
Reference in New Issue
Block a user