Add the Config view: edit and save the settings, applied without a restart
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-2
@@ -11,16 +11,34 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is a minimal JSON client for the Radarr/Sonarr v3 API.
|
||||
type Client struct {
|
||||
mu sync.RWMutex // guards baseURL and apiKey, which Reconfigure can change
|
||||
baseURL string
|
||||
apiKey string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// settings returns the current base URL and API key.
|
||||
func (c *Client) settings() (baseURL, apiKey string) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.baseURL, c.apiKey
|
||||
}
|
||||
|
||||
// Reconfigure points the client at another instance or key. Requests already
|
||||
// in flight are not affected.
|
||||
func (c *Client) Reconfigure(baseURL, apiKey string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.baseURL = strings.TrimRight(baseURL, "/")
|
||||
c.apiKey = apiKey
|
||||
}
|
||||
|
||||
// NewClient returns a client for the instance at baseURL (without /api/v3).
|
||||
func NewClient(baseURL, apiKey string) *Client {
|
||||
return &Client{
|
||||
@@ -46,7 +64,8 @@ func (e *APIError) Error() string {
|
||||
// Do sends a request to /api/v3/<path>. body (if non-nil) is JSON-encoded and
|
||||
// the response is decoded into out (if non-nil).
|
||||
func (c *Client) Do(ctx context.Context, method, path string, query url.Values, body, out any) error {
|
||||
u := c.baseURL + "/api/v3/" + strings.TrimLeft(path, "/")
|
||||
base, apiKey := c.settings()
|
||||
u := base + "/api/v3/" + strings.TrimLeft(path, "/")
|
||||
if len(query) > 0 {
|
||||
u += "?" + query.Encode()
|
||||
}
|
||||
@@ -64,7 +83,7 @@ func (c *Client) Do(ctx context.Context, method, path string, query url.Values,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-Api-Key", c.apiKey)
|
||||
req.Header.Set("X-Api-Key", apiKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -26,7 +26,8 @@ func (c *Client) Poster(images []Image, inLibrary bool) (key string, fetch func(
|
||||
}
|
||||
if inLibrary && img.URL != "" {
|
||||
local := img.URL
|
||||
return c.baseURL + "|" + local, func(ctx context.Context) ([]byte, error) {
|
||||
base, _ := c.settings()
|
||||
return base + "|" + local, func(ctx context.Context) ([]byte, error) {
|
||||
// MediaCover has poster-250.jpg next to poster.jpg; fall back to the original.
|
||||
if small := smallVariant(local); small != local {
|
||||
if data, err := c.GetBytes(ctx, small); err == nil {
|
||||
@@ -68,21 +69,23 @@ func (c *Client) GetBytes(ctx context.Context, ref string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Api-Key", c.apiKey)
|
||||
_, apiKey := c.settings()
|
||||
req.Header.Set("X-Api-Key", apiKey)
|
||||
return doImage(c.http, req)
|
||||
}
|
||||
|
||||
// resolve joins a server-relative path to the base URL. Radarr/Sonarr include
|
||||
// their URL base in image paths, so it must not be added twice.
|
||||
func (c *Client) resolve(ref string) (string, error) {
|
||||
base, err := url.Parse(c.baseURL)
|
||||
baseURL, _ := c.settings()
|
||||
base, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if base.Path != "" && base.Path != "/" && strings.HasPrefix(ref, strings.TrimRight(base.Path, "/")+"/") {
|
||||
return base.Scheme + "://" + base.Host + ref, nil
|
||||
}
|
||||
return c.baseURL + ref, nil
|
||||
return baseURL + ref, nil
|
||||
}
|
||||
|
||||
var imageClient = &http.Client{Timeout: 20 * time.Second}
|
||||
|
||||
@@ -46,3 +46,39 @@ func TestSetSeasonMonitoredUnknownSeason(t *testing.T) {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconfigureSwitchesInstanceAndKey(t *testing.T) {
|
||||
var gotKey, gotHost string
|
||||
mk := func(host string) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotKey, gotHost = r.Header.Get("X-Api-Key"), host
|
||||
io.WriteString(w, `{"version":"1"}`)
|
||||
}))
|
||||
}
|
||||
a, b := mk("a"), mk("b")
|
||||
defer a.Close()
|
||||
defer b.Close()
|
||||
|
||||
c := NewClient(a.URL, "key-a")
|
||||
var out struct{ Version string }
|
||||
if err := c.Do(context.Background(), http.MethodGet, "system/status", nil, nil, &out); err != nil || gotHost != "a" || gotKey != "key-a" {
|
||||
t.Fatalf("before: host=%q key=%q err=%v", gotHost, gotKey, err)
|
||||
}
|
||||
c.Reconfigure(b.URL+"/", "key-b") // a trailing slash is trimmed like in NewClient
|
||||
if err := c.Do(context.Background(), http.MethodGet, "system/status", nil, nil, &out); err != nil || gotHost != "b" || gotKey != "key-b" {
|
||||
t.Fatalf("after: host=%q key=%q err=%v", gotHost, gotKey, err)
|
||||
}
|
||||
// Used from many goroutines while being changed (run with -race to see it matter).
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for i := 0; i < 50; i++ {
|
||||
c.Reconfigure(a.URL, "k")
|
||||
c.Reconfigure(b.URL, "k")
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
for i := 0; i < 50; i++ {
|
||||
_ = c.Do(context.Background(), http.MethodGet, "system/status", nil, nil, &out)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user