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>
160 lines
4.7 KiB
Go
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(),
|
|
})
|
|
}
|