package api import ( "encoding/json" "errors" "net/http" "time" "github.com/go-chi/chi/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/coverart" ) // coverProviderResp is the wire shape for one provider in the // GET /api/admin/cover-sources list. Mirrors coverart.ProviderInfo // but uses snake_case JSON tags. type coverProviderResp struct { ID string `json:"id"` DisplayName string `json:"display_name"` RequiresAPIKey bool `json:"requires_api_key"` Supports []string `json:"supports"` Enabled bool `json:"enabled"` APIKeySet bool `json:"api_key_set"` DisplayOrder int32 `json:"display_order"` Testable bool `json:"testable"` } type coverProvidersListResp struct { Providers []coverProviderResp `json:"providers"` SourcesVersion int32 `json:"sources_version"` } // handleListCoverSources implements GET /api/admin/cover-sources. func (h *handlers) handleListCoverSources(w http.ResponseWriter, _ *http.Request) { infos := h.coverSettings.ListProviderInfo() out := coverProvidersListResp{ Providers: make([]coverProviderResp, 0, len(infos)), SourcesVersion: h.coverSettings.CurrentVersion(), } for _, info := range infos { supports := info.Supports if supports == nil { supports = []string{} } out.Providers = append(out.Providers, coverProviderResp{ ID: info.ID, DisplayName: info.DisplayName, RequiresAPIKey: info.RequiresAPIKey, Supports: supports, Enabled: info.Enabled, APIKeySet: info.APIKeySet, DisplayOrder: info.DisplayOrder, Testable: info.Testable, }) } writeJSON(w, http.StatusOK, out) } // updateCoverSourceReq is the PATCH body. nil pointers mean // "leave unchanged"; non-nil means "set to this value (which may be // empty string for api_key clearing)". type updateCoverSourceReq struct { Enabled *bool `json:"enabled,omitempty"` APIKey *string `json:"api_key,omitempty"` } type updateCoverSourceResp struct { coverProviderResp VersionBumped bool `json:"version_bumped"` } // handleUpdateCoverSource implements PATCH /api/admin/cover-sources/{provider_id}. func (h *handlers) handleUpdateCoverSource(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "provider_id") var body updateCoverSourceReq if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeErr(w, apierror.BadRequest("bad_body", "invalid JSON")) return } bumped, err := h.coverSettings.UpdateProvider(r.Context(), id, coverart.ProviderUpdatePatch{ Enabled: body.Enabled, APIKey: body.APIKey, }) if err != nil { if errors.Is(err, coverart.ErrProviderNotFound) { writeErr(w, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "no such provider"}) return } h.logger.Error("admin: update cover source", "id", id, "err", err) writeErr(w, apierror.InternalMsg("update failed", err)) return } // Re-fetch the provider info to return. infos := h.coverSettings.ListProviderInfo() var found coverProviderResp for _, info := range infos { if info.ID == id { supports := info.Supports if supports == nil { supports = []string{} } found = coverProviderResp{ ID: info.ID, DisplayName: info.DisplayName, RequiresAPIKey: info.RequiresAPIKey, Supports: supports, Enabled: info.Enabled, APIKeySet: info.APIKeySet, DisplayOrder: info.DisplayOrder, Testable: info.Testable, } break } } writeJSON(w, http.StatusOK, updateCoverSourceResp{ coverProviderResp: found, VersionBumped: bumped, }) } type testCoverSourceResp struct { OK bool `json:"ok"` DurationMs int64 `json:"duration_ms,omitempty"` Error string `json:"error,omitempty"` } // handleTestCoverSource implements POST /api/admin/cover-sources/{provider_id}/test. func (h *handlers) handleTestCoverSource(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "provider_id") start := time.Now() err := h.coverSettings.TestProvider(r.Context(), id) duration := time.Since(start).Milliseconds() if err == nil { writeJSON(w, http.StatusOK, testCoverSourceResp{OK: true, DurationMs: duration}) return } if errors.Is(err, coverart.ErrProviderNotFound) { writeErr(w, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "no such provider"}) return } // Not testable, or test failed — both surface as ok=false. // 200 status because the operation completed; the test result is // data, not an error. writeJSON(w, http.StatusOK, testCoverSourceResp{ OK: false, DurationMs: duration, Error: err.Error(), }) }