diff --git a/internal/api/admin_lidarr.go b/internal/api/admin_lidarr.go index 0041214b..7eea6dfc 100644 --- a/internal/api/admin_lidarr.go +++ b/internal/api/admin_lidarr.go @@ -1,10 +1,12 @@ package api import ( + "context" "encoding/json" "errors" "net/http" "net/url" + "sync" "git.fabledsword.com/bvandeusen/minstrel/internal/lidarr" "git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig" @@ -131,6 +133,24 @@ type testLidarrBody struct { 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. // Always returns 200; the ok/error fields in the response body indicate // connection success or failure. @@ -169,10 +189,85 @@ func (h *handlers) handleTestLidarrConnection(w http.ResponseWriter, r *http.Req // must NEVER be logged. h.logger.Warn("admin: test lidarr ping failed", "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 } - 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