feat(server/admin): test endpoint returns profiles + folders on success
Extends POST /api/admin/lidarr/test response to include quality_profiles, metadata_profiles, root_folders, and an optional list_errors map when the Lidarr connection succeeds. The three list fetches run in parallel after the ping; any per-list failure goes into list_errors so the response can still report ok=true with whatever data did come back. Failed-ping response shape is unchanged. This lets /admin/integrations populate its dropdowns on a single round-trip during first-time setup, fixing the chicken-and-egg where the dropdowns previously gated on cfg.Enabled (which can't be true until the first save, which itself needs non-zero defaults). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
|
||||||
@@ -131,6 +133,24 @@ type testLidarrBody struct {
|
|||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testLidarrResponse is the JSON shape returned by POST /api/admin/lidarr/test.
|
||||||
|
// All fields except Ok are optional. On a failed connection (Ok=false), only
|
||||||
|
// Ok and Error are populated. On success (Ok=true), Version is populated;
|
||||||
|
// QualityProfiles / MetadataProfiles / RootFolders carry the live Lidarr
|
||||||
|
// data and ListErrors maps any per-list fetch failures by list name. The
|
||||||
|
// integrations page consumes the lists to pre-fill its dropdowns on
|
||||||
|
// first-time setup, fixing the chicken-and-egg where the dropdowns
|
||||||
|
// previously gated on cfg.Enabled.
|
||||||
|
type testLidarrResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
QualityProfiles []qualityProfileView `json:"quality_profiles,omitempty"`
|
||||||
|
MetadataProfiles []metadataProfileView `json:"metadata_profiles,omitempty"`
|
||||||
|
RootFolders []rootFolderView `json:"root_folders,omitempty"`
|
||||||
|
ListErrors map[string]string `json:"list_errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// handleTestLidarrConnection implements POST /api/admin/lidarr/test.
|
// handleTestLidarrConnection implements POST /api/admin/lidarr/test.
|
||||||
// Always returns 200; the ok/error fields in the response body indicate
|
// Always returns 200; the ok/error fields in the response body indicate
|
||||||
// connection success or failure.
|
// connection success or failure.
|
||||||
@@ -169,10 +189,85 @@ func (h *handlers) handleTestLidarrConnection(w http.ResponseWriter, r *http.Req
|
|||||||
// must NEVER be logged.
|
// must NEVER be logged.
|
||||||
h.logger.Warn("admin: test lidarr ping failed",
|
h.logger.Warn("admin: test lidarr ping failed",
|
||||||
"err", err, "base_url", baseURL, "code", errCode)
|
"err", err, "base_url", baseURL, "code", errCode)
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"ok": false, "error": errCode})
|
writeJSON(w, http.StatusOK, testLidarrResponse{Ok: false, Error: errCode})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "version": result.Version})
|
quality, metadata, folders, listErrs := fetchLidarrLists(r.Context(), client)
|
||||||
|
writeJSON(w, http.StatusOK, testLidarrResponse{
|
||||||
|
Ok: true,
|
||||||
|
Version: result.Version,
|
||||||
|
QualityProfiles: quality,
|
||||||
|
MetadataProfiles: metadata,
|
||||||
|
RootFolders: folders,
|
||||||
|
ListErrors: listErrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchLidarrLists fans out the three list calls in parallel and returns
|
||||||
|
// the lists plus a map of any per-list errors. Empty map (returned as nil
|
||||||
|
// to omit the JSON field) if all succeeded. Used by the test handler to
|
||||||
|
// pre-populate the integrations page dropdowns on the same round-trip as
|
||||||
|
// the connection check.
|
||||||
|
func fetchLidarrLists(ctx context.Context, client *lidarr.Client) (
|
||||||
|
[]qualityProfileView, []metadataProfileView, []rootFolderView, map[string]string,
|
||||||
|
) {
|
||||||
|
var (
|
||||||
|
quality []qualityProfileView
|
||||||
|
metadata []metadataProfileView
|
||||||
|
folders []rootFolderView
|
||||||
|
errMu sync.Mutex
|
||||||
|
errs = map[string]string{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
)
|
||||||
|
recordErr := func(name string, err error) {
|
||||||
|
errMu.Lock()
|
||||||
|
errs[name] = lidarrErrCode(err)
|
||||||
|
errMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(3)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
ps, err := client.ListQualityProfiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
recordErr("quality_profiles", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
quality = make([]qualityProfileView, len(ps))
|
||||||
|
for i, p := range ps {
|
||||||
|
quality[i] = qualityProfileView{ID: p.ID, Name: p.Name}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
ps, err := client.ListMetadataProfiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
recordErr("metadata_profiles", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metadata = make([]metadataProfileView, len(ps))
|
||||||
|
for i, p := range ps {
|
||||||
|
metadata[i] = metadataProfileView{ID: p.ID, Name: p.Name}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
fs, err := client.ListRootFolders(ctx)
|
||||||
|
if err != nil {
|
||||||
|
recordErr("root_folders", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
folders = make([]rootFolderView, len(fs))
|
||||||
|
for i, f := range fs {
|
||||||
|
folders[i] = rootFolderView{Path: f.Path, Accessible: f.Accessible, FreeSpace: f.FreeSpace}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return quality, metadata, folders, nil
|
||||||
|
}
|
||||||
|
return quality, metadata, folders, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// lidarrErrCode maps a lidarr client error to the stable string code used in
|
// lidarrErrCode maps a lidarr client error to the stable string code used in
|
||||||
|
|||||||
Reference in New Issue
Block a user