feat(api): admin tag-sources endpoints (#1490 Step 4 backend)
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:
@@ -326,6 +326,7 @@ func run() error {
|
||||
srv.Bus = bus
|
||||
srv.PlaylistScheduler = playlistScheduler
|
||||
srv.RecSettings = recSettings
|
||||
srv.TagSettings = tagSettings
|
||||
srv.StreamSecret = cfg.StreamSecret
|
||||
httpServer := &http.Server{
|
||||
Addr: cfg.Server.Address,
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
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/tags"
|
||||
)
|
||||
|
||||
// tagSourceResp is the wire shape for one tag-enrichment provider in the
|
||||
// GET /api/admin/tag-sources list. Mirrors the cover-sources admin surface
|
||||
// (admin_cover_sources.go) — a separate, independent settings card so a new
|
||||
// tag source is added without touching art settings (#1490).
|
||||
type tagSourceResp 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"`
|
||||
}
|
||||
|
||||
func tagSourceRespFrom(info tags.ProviderInfo) tagSourceResp {
|
||||
supports := info.Supports
|
||||
if supports == nil {
|
||||
supports = []string{}
|
||||
}
|
||||
return tagSourceResp{
|
||||
ID: info.ID,
|
||||
DisplayName: info.DisplayName,
|
||||
RequiresAPIKey: info.RequiresAPIKey,
|
||||
Supports: supports,
|
||||
Enabled: info.Enabled,
|
||||
APIKeySet: info.APIKeySet,
|
||||
DisplayOrder: info.DisplayOrder,
|
||||
Testable: info.Testable,
|
||||
}
|
||||
}
|
||||
|
||||
type tagSourcesListResp struct {
|
||||
Providers []tagSourceResp `json:"providers"`
|
||||
SourcesVersion int32 `json:"sources_version"`
|
||||
}
|
||||
|
||||
// handleListTagSources implements GET /api/admin/tag-sources.
|
||||
func (h *handlers) handleListTagSources(w http.ResponseWriter, _ *http.Request) {
|
||||
infos := h.tagSettings.ListProviderInfo()
|
||||
out := tagSourcesListResp{
|
||||
Providers: make([]tagSourceResp, 0, len(infos)),
|
||||
SourcesVersion: h.tagSettings.CurrentVersion(),
|
||||
}
|
||||
for _, info := range infos {
|
||||
out.Providers = append(out.Providers, tagSourceRespFrom(info))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// updateTagSourceReq is the PATCH body. nil pointers mean "leave
|
||||
// unchanged"; non-nil sets (empty api_key clears).
|
||||
type updateTagSourceReq struct {
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
APIKey *string `json:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
type updateTagSourceResp struct {
|
||||
tagSourceResp
|
||||
VersionBumped bool `json:"version_bumped"`
|
||||
}
|
||||
|
||||
// handleUpdateTagSource implements PATCH /api/admin/tag-sources/{provider_id}.
|
||||
func (h *handlers) handleUpdateTagSource(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "provider_id")
|
||||
|
||||
var body updateTagSourceReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeErr(w, apierror.BadRequest("bad_body", "invalid JSON"))
|
||||
return
|
||||
}
|
||||
|
||||
bumped, err := h.tagSettings.UpdateProvider(r.Context(), id, tags.ProviderUpdatePatch{
|
||||
Enabled: body.Enabled,
|
||||
APIKey: body.APIKey,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, tags.ErrProviderNotFound) {
|
||||
writeErr(w, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "no such provider"})
|
||||
return
|
||||
}
|
||||
h.logger.Error("admin: update tag source", "id", id, "err", err)
|
||||
writeErr(w, apierror.InternalMsg("update failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
var found tagSourceResp
|
||||
for _, info := range h.tagSettings.ListProviderInfo() {
|
||||
if info.ID == id {
|
||||
found = tagSourceRespFrom(info)
|
||||
break
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, updateTagSourceResp{tagSourceResp: found, VersionBumped: bumped})
|
||||
}
|
||||
|
||||
type testTagSourceResp struct {
|
||||
OK bool `json:"ok"`
|
||||
DurationMs int64 `json:"duration_ms,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// handleTestTagSource implements POST /api/admin/tag-sources/{provider_id}/test.
|
||||
func (h *handlers) handleTestTagSource(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "provider_id")
|
||||
|
||||
start := time.Now()
|
||||
err := h.tagSettings.TestProvider(r.Context(), id)
|
||||
duration := time.Since(start).Milliseconds()
|
||||
|
||||
if err == nil {
|
||||
writeJSON(w, http.StatusOK, testTagSourceResp{OK: true, DurationMs: duration})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, tags.ErrProviderNotFound) {
|
||||
writeErr(w, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "no such provider"})
|
||||
return
|
||||
}
|
||||
// Not testable, or the test failed — both surface as ok=false with a
|
||||
// 200 (the operation completed; the result is data, not an error).
|
||||
writeJSON(w, http.StatusOK, testTagSourceResp{OK: false, DurationMs: duration, Error: err.Error()})
|
||||
}
|
||||
|
||||
type researchTagsResp struct {
|
||||
SourcesVersion int32 `json:"sources_version"`
|
||||
}
|
||||
|
||||
// handleResearchTags implements POST /api/admin/tag-sources/research: bump
|
||||
// the sources version so every settled ('none') track becomes eligible for
|
||||
// a re-enrichment pass, then the background worker re-processes them.
|
||||
func (h *handlers) handleResearchTags(w http.ResponseWriter, r *http.Request) {
|
||||
newVer, err := h.tagSettings.BumpVersion(r.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("admin: research tags", "err", err)
|
||||
writeErr(w, apierror.InternalMsg("research failed", err))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, researchTagsResp{SourcesVersion: newVer})
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/tags"
|
||||
)
|
||||
|
||||
// apiTestTagProvider is a minimal TrackTagProvider for api-package tests.
|
||||
// Does not implement TestableProvider.
|
||||
type apiTestTagProvider struct {
|
||||
id string
|
||||
display string
|
||||
}
|
||||
|
||||
func (p *apiTestTagProvider) ID() string { return p.id }
|
||||
func (p *apiTestTagProvider) DisplayName() string { return p.display }
|
||||
func (p *apiTestTagProvider) RequiresAPIKey() bool { return false }
|
||||
func (p *apiTestTagProvider) DefaultEnabled() bool { return true }
|
||||
func (p *apiTestTagProvider) Configure(_ tags.ProviderSettings) error { return nil }
|
||||
func (p *apiTestTagProvider) FetchTrackTags(_ context.Context, _ tags.TrackRef) ([]tags.Tag, error) {
|
||||
return []tags.Tag{{Name: "x", Weight: 1}}, nil
|
||||
}
|
||||
|
||||
// apiTestTestableTagProvider also implements TestableProvider.
|
||||
type apiTestTestableTagProvider struct {
|
||||
apiTestTagProvider
|
||||
}
|
||||
|
||||
func (p *apiTestTestableTagProvider) TestConnection(_ context.Context) error { return nil }
|
||||
|
||||
func newAdminTagSourcesRouter(h *handlers) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
r.Route("/api/admin", func(admin chi.Router) {
|
||||
admin.Use(auth.RequireAdmin())
|
||||
admin.Get("/tag-sources", h.handleListTagSources)
|
||||
admin.Patch("/tag-sources/{provider_id}", h.handleUpdateTagSource)
|
||||
admin.Post("/tag-sources/{provider_id}/test", h.handleTestTagSource)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// testHandlersWithTagSettings installs a tags.SettingsService backed by the
|
||||
// test DB. Register the needed fake providers BEFORE calling this, and pair
|
||||
// with tags.ResetRegistryForTests in t.Cleanup.
|
||||
func testHandlersWithTagSettings(t *testing.T) *handlers {
|
||||
t.Helper()
|
||||
h, pool := testHandlers(t)
|
||||
s, err := tags.NewSettingsService(context.Background(), pool, h.logger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSettingsService: %v", err)
|
||||
}
|
||||
h.tagSettings = s
|
||||
return h
|
||||
}
|
||||
|
||||
func TestAdminListTagSources_ReturnsRegisteredProviders(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
tags.ResetRegistryForTests()
|
||||
t.Cleanup(tags.ResetRegistryForTests)
|
||||
tags.Register(&apiTestTagProvider{id: "test-tag-prov", display: "Test Tag Provider"})
|
||||
|
||||
h := testHandlersWithTagSettings(t)
|
||||
admin := seedUser(t, h.pool, "tslist", "pw", true)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/tag-sources", nil)
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminTagSourcesRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp tagSourcesListResp
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(resp.Providers) != 1 || resp.Providers[0].ID != "test-tag-prov" {
|
||||
t.Fatalf("providers = %+v, want 1 (test-tag-prov)", resp.Providers)
|
||||
}
|
||||
found := false
|
||||
for _, s := range resp.Providers[0].Supports {
|
||||
if s == "track_tags" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("supports = %v, want to include track_tags", resp.Providers[0].Supports)
|
||||
}
|
||||
if resp.Providers[0].Testable {
|
||||
t.Error("testable = true, want false (no TestConnection)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUpdateTagSource_FlippingEnabledBumpsVersion(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
tags.ResetRegistryForTests()
|
||||
t.Cleanup(tags.ResetRegistryForTests)
|
||||
tags.Register(&apiTestTagProvider{id: "flip-tag", display: "Flip Tag"})
|
||||
|
||||
h := testHandlersWithTagSettings(t)
|
||||
admin := seedUser(t, h.pool, "tsflip", "pw", true)
|
||||
|
||||
disabled := false
|
||||
body, _ := json.Marshal(updateTagSourceReq{Enabled: &disabled})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/admin/tag-sources/flip-tag", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminTagSourcesRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp updateTagSourceResp
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if !resp.VersionBumped {
|
||||
t.Error("version_bumped = false, want true after flipping enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUpdateTagSource_KeyOnlyDoesNotBump(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
tags.ResetRegistryForTests()
|
||||
t.Cleanup(tags.ResetRegistryForTests)
|
||||
tags.Register(&apiTestTagProvider{id: "key-tag", display: "Key Tag"})
|
||||
|
||||
h := testHandlersWithTagSettings(t)
|
||||
admin := seedUser(t, h.pool, "tskey", "pw", true)
|
||||
|
||||
newKey := "newkey"
|
||||
body, _ := json.Marshal(updateTagSourceReq{APIKey: &newKey})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/admin/tag-sources/key-tag", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminTagSourcesRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp updateTagSourceResp
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.VersionBumped {
|
||||
t.Error("version_bumped = true, want false for key-only change")
|
||||
}
|
||||
if !resp.APIKeySet {
|
||||
t.Error("api_key_set = false, want true after setting key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUpdateTagSource_UnknownProvider404(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
tags.ResetRegistryForTests()
|
||||
t.Cleanup(tags.ResetRegistryForTests)
|
||||
|
||||
h := testHandlersWithTagSettings(t)
|
||||
admin := seedUser(t, h.pool, "ts404", "pw", true)
|
||||
|
||||
body, _ := json.Marshal(updateTagSourceReq{})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/admin/tag-sources/missing", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminTagSourcesRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminTestTagSource_NonAdminReturns403(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
tags.ResetRegistryForTests()
|
||||
t.Cleanup(tags.ResetRegistryForTests)
|
||||
tags.Register(&apiTestTestableTagProvider{apiTestTagProvider{id: "testable-tag", display: "Testable Tag"}})
|
||||
|
||||
h := testHandlersWithTagSettings(t)
|
||||
nonAdmin := seedUser(t, h.pool, "tsnoadmin", "pw", false)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/admin/tag-sources/testable-tag/test", nil)
|
||||
req = withUser(req, nonAdmin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminTagSourcesRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("status = %d, want 403; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminTestTagSource_NotTestableReturnsOkFalse(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
tags.ResetRegistryForTests()
|
||||
t.Cleanup(tags.ResetRegistryForTests)
|
||||
tags.Register(&apiTestTagProvider{id: "nontestable-tag", display: "Non-Testable Tag"})
|
||||
|
||||
h := testHandlersWithTagSettings(t)
|
||||
admin := seedUser(t, h.pool, "tsntest", "pw", true)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/admin/tag-sources/nontestable-tag/test", nil)
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminTagSourcesRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp testTagSourceResp
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.OK {
|
||||
t.Error("ok = true, want false for non-testable provider")
|
||||
}
|
||||
if resp.Error == "" {
|
||||
t.Error("error string empty, want a message")
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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
|
||||
|
||||
@@ -465,7 +465,7 @@ func TestRoutesRegisteredInMount(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
w := playevents.NewWriter(h.pool, slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
30*time.Minute, 0.5, 30000)
|
||||
Mount(r, h.pool, h.logger, w, config.RecommendationConfig{RadioSize: 50, RadioSizeMax: 200, RecentlyPlayedHours: 1}, h.recSettings, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverSettings, h.scanner, h.scanCfg, h.dataDir, nil, eventbus.New(), nil, nil)
|
||||
Mount(r, h.pool, h.logger, w, config.RecommendationConfig{RadioSize: 50, RadioSizeMax: 200, RecentlyPlayedHours: 1}, h.recSettings, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverSettings, h.tagSettings, h.scanner, h.scanCfg, h.dataDir, nil, eventbus.New(), nil, nil)
|
||||
|
||||
paths := []string{
|
||||
"/api/artists",
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/playlists"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/recsettings"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/tags"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/tracks"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/web"
|
||||
)
|
||||
@@ -77,6 +78,7 @@ type Server struct {
|
||||
BrandingCfg config.BrandingConfig
|
||||
CoverEnricher *coverart.Enricher
|
||||
CoverSettings *coverart.SettingsService
|
||||
TagSettings *tags.SettingsService
|
||||
LibraryScanner *library.Scanner
|
||||
ScanCfg library.RunScanConfig
|
||||
// Bus is the live-event bus shared with background workers (the
|
||||
@@ -162,7 +164,7 @@ func (s *Server) Router() http.Handler {
|
||||
s.Logger.Error("server: recsettings boot failed", "err", err)
|
||||
}
|
||||
}
|
||||
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, recSettings, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.DataDir, smtpSender, bus, s.PlaylistScheduler, s.StreamSecret)
|
||||
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, recSettings, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverSettings, s.TagSettings, s.LibraryScanner, s.ScanCfg, s.DataDir, smtpSender, bus, s.PlaylistScheduler, s.StreamSecret)
|
||||
// /api/admin/scan is the only admin route owned by the server package
|
||||
// (it needs the Scanner). Register it as a single inline-middleware
|
||||
// route — using r.Route("/api/admin", ...) here would create a second
|
||||
|
||||
Reference in New Issue
Block a user