Files
minstrel/internal/api/admin_cover_sources.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

160 lines
4.7 KiB
Go

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(),
})
}