feat(api): admin tag-sources endpoints (#1490 Step 4 backend)
test-go / test (push) Successful in 29s
test-go / integration (push) Failing after 4m43s

Expose the tag-enrichment provider settings over the admin API, mirroring
the cover-sources surface so a Last.fm key can be pasted and sources
toggled from the web admin UI (rules #25/#27).

- GET  /api/admin/tag-sources                 — list providers + version
- PATCH/api/admin/tag-sources/{id}            — enable / set api_key
- POST /api/admin/tag-sources/{id}/test       — test connection
- POST /api/admin/tag-sources/research        — bump version, re-open
                                                 settled rows for re-enrich
- Thread tags.SettingsService through server.New (struct field, like
  RecSettings) → Router → api.Mount → handlers.tagSettings; main.go sets
  srv.TagSettings.

Handler tests mirror admin_cover_sources_test (list / flip-bumps-version /
key-only-no-bump / unknown-404 / non-admin-403 / not-testable-ok-false),
integration-tier (skip without MINSTREL_TEST_DATABASE_URL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:24:28 -04:00
parent 7d18a3c808
commit cce928e584
6 changed files with 410 additions and 3 deletions
+9 -1
View File
@@ -23,13 +23,14 @@ import (
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
"git.fabledsword.com/bvandeusen/minstrel/internal/playlists"
"git.fabledsword.com/bvandeusen/minstrel/internal/recsettings"
"git.fabledsword.com/bvandeusen/minstrel/internal/tags"
"git.fabledsword.com/bvandeusen/minstrel/internal/tracks"
)
// Mount attaches /api/* handlers to r. Public endpoints (login) are outside
// RequireUser; everything else is gated by the middleware. The events writer
// is shared with the Subsonic mount so /rest/scrobble feeds the same store.
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playevents.Writer, recCfg config.RecommendationConfig, recSettings *recsettings.Service, lidarrCfg *lidarrconfig.Service, lidarrReqs *lidarrrequests.Service, lidarrQuar *lidarrquarantine.Service, tracksSvc *tracks.Service, playlistsSvc *playlists.Service, coverEnricher *coverart.Enricher, coverSettings *coverart.SettingsService, scanner *library.Scanner, scanCfg library.RunScanConfig, dataDir string, sender mailer.Sender, bus *eventbus.Bus, playlistScheduler *playlists.Scheduler, streamSecret []byte) {
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playevents.Writer, recCfg config.RecommendationConfig, recSettings *recsettings.Service, lidarrCfg *lidarrconfig.Service, lidarrReqs *lidarrrequests.Service, lidarrQuar *lidarrquarantine.Service, tracksSvc *tracks.Service, playlistsSvc *playlists.Service, coverEnricher *coverart.Enricher, coverSettings *coverart.SettingsService, tagSettings *tags.SettingsService, scanner *library.Scanner, scanCfg library.RunScanConfig, dataDir string, sender mailer.Sender, bus *eventbus.Bus, playlistScheduler *playlists.Scheduler, streamSecret []byte) {
rng := rand.New(rand.NewSource(rand.Int63()))
h := &handlers{
pool: pool, logger: logger, events: events, recCfg: recCfg,
@@ -42,6 +43,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
playlists: playlistsSvc,
coverart: coverEnricher,
coverSettings: coverSettings,
tagSettings: tagSettings,
scanner: scanner,
scanCfg: scanCfg,
dataDir: dataDir,
@@ -200,6 +202,11 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
admin.Post("/cover-sources/{provider_id}/test", h.handleTestCoverSource)
admin.Post("/cover-sources/research", h.handleResearchMissingArt)
admin.Get("/tag-sources", h.handleListTagSources)
admin.Patch("/tag-sources/{provider_id}", h.handleUpdateTagSource)
admin.Post("/tag-sources/{provider_id}/test", h.handleTestTagSource)
admin.Post("/tag-sources/research", h.handleResearchTags)
admin.Get("/smtp-config", h.handleGetSMTPConfig)
admin.Put("/smtp-config", h.handleUpdateSMTPConfig)
admin.Post("/smtp-config/test", h.handleTestSMTPConfig)
@@ -242,6 +249,7 @@ type handlers struct {
playlists *playlists.Service
coverart *coverart.Enricher
coverSettings *coverart.SettingsService
tagSettings *tags.SettingsService
scanner *library.Scanner
scanCfg library.RunScanConfig
dataDir string