feat(server/m7-recurring-scans): admin schedule HTTP handlers
Two new endpoints under existing RequireAdmin middleware: - GET /api/admin/scan/schedule — returns config + computed next_scheduled_at (ISO 8601; null when mode='off') - PATCH /api/admin/scan/schedule — validates per-mode required fields, normalizes mode='off' to NULL the per-mode fields, writes via UpdateScanSchedule, calls scheduler.Refresh(), returns the updated record. server.New + api.Mount gain a *library.Scheduler parameter; handlers struct + Server struct gain the scheduler field. Test stubs (server_test.go, library_test.go's Mount call) updated to pass nil. Tests gated on MINSTREL_TEST_DATABASE_URL: GET default is mode=off with null next; PATCH daily produces a 03:00 next-fire; PATCH weekly without weekly_day → 400; PATCH off normalizes lingering per-mode fields to NULL; non-admin → 403. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -166,10 +166,9 @@ func run() error {
|
||||
scheduler := library.NewScheduler(pool, logger.With("component", "scheduler"),
|
||||
scanner, coverEnricher, scanCfg)
|
||||
scheduler.Start(ctx)
|
||||
_ = scheduler // keep reachable for future wiring (Task 4)
|
||||
srv := server.New(logger, pool, scanner, subsonic.Config{
|
||||
AllowPlaintextPassword: cfg.Subsonic.AllowPlaintextPassword,
|
||||
}, cfg.Events, cfg.Recommendation, cfg.Storage.DataDir, cfg.Branding, coverEnricher, cfg.Library.CoverArtBackfillCap, coverSettings, scanner, scanCfg)
|
||||
}, cfg.Events, cfg.Recommendation, cfg.Storage.DataDir, cfg.Branding, coverEnricher, cfg.Library.CoverArtBackfillCap, coverSettings, scanner, scanCfg, scheduler)
|
||||
playlists.StartSystemPlaylistCron(ctx, pool, logger.With("component", "system_playlist_cron"))
|
||||
httpServer := &http.Server{
|
||||
Addr: cfg.Server.Address,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||
)
|
||||
|
||||
type scanScheduleResp struct {
|
||||
Mode string `json:"mode"`
|
||||
IntervalHours *int `json:"interval_hours"`
|
||||
TimeOfDay *string `json:"time_of_day"`
|
||||
WeeklyDay *int `json:"weekly_day"`
|
||||
NextScheduledAt *string `json:"next_scheduled_at"`
|
||||
}
|
||||
|
||||
type scanScheduleReq struct {
|
||||
Mode string `json:"mode"`
|
||||
IntervalHours *int `json:"interval_hours"`
|
||||
TimeOfDay *string `json:"time_of_day"`
|
||||
WeeklyDay *int `json:"weekly_day"`
|
||||
}
|
||||
|
||||
func (h *handlers) handleGetScanSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
q := dbq.New(h.pool)
|
||||
row, err := q.GetScanSchedule(r.Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
h.logger.Error("admin: scan_schedule singleton row missing")
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "schedule row missing")
|
||||
return
|
||||
}
|
||||
h.logger.Error("admin: get scan schedule", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, scanScheduleRespFromRow(row, h.scheduler.NextFire()))
|
||||
}
|
||||
|
||||
func (h *handlers) handlePatchScanSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
var req scanScheduleReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "bad_body", "invalid JSON")
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateScheduleReq(&req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "validation", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
q := dbq.New(h.pool)
|
||||
if err := q.UpdateScanSchedule(r.Context(), dbq.UpdateScanScheduleParams{
|
||||
Mode: req.Mode,
|
||||
IntervalHours: pgInt32Ptr(req.IntervalHours),
|
||||
TimeOfDay: req.TimeOfDay,
|
||||
WeeklyDay: pgInt32Ptr(req.WeeklyDay),
|
||||
}); err != nil {
|
||||
h.logger.Error("admin: update scan schedule", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "update failed")
|
||||
return
|
||||
}
|
||||
|
||||
h.scheduler.Refresh()
|
||||
|
||||
row, err := q.GetScanSchedule(r.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("admin: re-read scan schedule", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "re-read failed")
|
||||
return
|
||||
}
|
||||
cfg := library.ScheduleConfig{Mode: row.Mode}
|
||||
if row.IntervalHours != nil {
|
||||
cfg.IntervalHours = int(*row.IntervalHours)
|
||||
}
|
||||
if row.TimeOfDay != nil {
|
||||
cfg.TimeOfDay = *row.TimeOfDay
|
||||
}
|
||||
if row.WeeklyDay != nil {
|
||||
cfg.WeeklyDay = int(*row.WeeklyDay)
|
||||
}
|
||||
next := cfg.NextFire(time.Now())
|
||||
|
||||
writeJSON(w, http.StatusOK, scanScheduleRespFromRow(row, next))
|
||||
}
|
||||
|
||||
func validateScheduleReq(req *scanScheduleReq) error {
|
||||
switch req.Mode {
|
||||
case "off":
|
||||
req.IntervalHours = nil
|
||||
req.TimeOfDay = nil
|
||||
req.WeeklyDay = nil
|
||||
return nil
|
||||
case "interval":
|
||||
if req.IntervalHours == nil || *req.IntervalHours <= 0 {
|
||||
return fmt.Errorf("interval_hours required and > 0 when mode=interval")
|
||||
}
|
||||
req.TimeOfDay = nil
|
||||
req.WeeklyDay = nil
|
||||
return nil
|
||||
case "daily":
|
||||
if req.TimeOfDay == nil || *req.TimeOfDay == "" {
|
||||
return fmt.Errorf("time_of_day required when mode=daily")
|
||||
}
|
||||
req.IntervalHours = nil
|
||||
req.WeeklyDay = nil
|
||||
return nil
|
||||
case "weekly":
|
||||
if req.TimeOfDay == nil || *req.TimeOfDay == "" {
|
||||
return fmt.Errorf("time_of_day required when mode=weekly")
|
||||
}
|
||||
if req.WeeklyDay == nil || *req.WeeklyDay < 1 || *req.WeeklyDay > 7 {
|
||||
return fmt.Errorf("weekly_day required and in [1, 7] when mode=weekly")
|
||||
}
|
||||
req.IntervalHours = nil
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid mode %q (want off / interval / daily / weekly)", req.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func scanScheduleRespFromRow(row dbq.GetScanScheduleRow, nextFire time.Time) scanScheduleResp {
|
||||
resp := scanScheduleResp{Mode: row.Mode}
|
||||
if row.IntervalHours != nil {
|
||||
v := int(*row.IntervalHours)
|
||||
resp.IntervalHours = &v
|
||||
}
|
||||
if row.TimeOfDay != nil {
|
||||
v := *row.TimeOfDay
|
||||
resp.TimeOfDay = &v
|
||||
}
|
||||
if row.WeeklyDay != nil {
|
||||
v := int(*row.WeeklyDay)
|
||||
resp.WeeklyDay = &v
|
||||
}
|
||||
if !nextFire.IsZero() {
|
||||
s := nextFire.UTC().Format(time.RFC3339)
|
||||
resp.NextScheduledAt = &s
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func pgInt32Ptr(src *int) *int32 {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
v := int32(*src)
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||
)
|
||||
|
||||
func newAdminScheduleRouter(h *handlers) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
r.Route("/api/admin", func(admin chi.Router) {
|
||||
admin.Use(auth.RequireAdmin())
|
||||
admin.Get("/scan/schedule", h.handleGetScanSchedule)
|
||||
admin.Patch("/scan/schedule", h.handlePatchScanSchedule)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func TestAdminScanSchedule_Get_DefaultIsOff(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
h, pool := testHandlers(t)
|
||||
s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{})
|
||||
h.scheduler = s
|
||||
admin := seedUser(t, pool, "schedget", "pw", true)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/scan/schedule", nil)
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminScheduleRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp scanScheduleResp
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Mode != "off" {
|
||||
t.Errorf("Mode = %q, want off", resp.Mode)
|
||||
}
|
||||
if resp.NextScheduledAt != nil {
|
||||
t.Errorf("NextScheduledAt = %v, want nil for mode=off", resp.NextScheduledAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminScanSchedule_Patch_DailyMode(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
h, pool := testHandlers(t)
|
||||
s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{})
|
||||
h.scheduler = s
|
||||
admin := seedUser(t, pool, "schedpatch", "pw", true)
|
||||
|
||||
body := `{"mode":"daily","time_of_day":"03:00"}`
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/admin/scan/schedule",
|
||||
bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminScheduleRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp scanScheduleResp
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Mode != "daily" {
|
||||
t.Errorf("Mode = %q, want daily", resp.Mode)
|
||||
}
|
||||
if resp.TimeOfDay == nil || *resp.TimeOfDay != "03:00" {
|
||||
t.Errorf("TimeOfDay = %v, want '03:00'", resp.TimeOfDay)
|
||||
}
|
||||
if resp.NextScheduledAt == nil {
|
||||
t.Errorf("NextScheduledAt = nil, want a future ISO timestamp")
|
||||
} else {
|
||||
next, err := time.Parse(time.RFC3339, *resp.NextScheduledAt)
|
||||
if err != nil {
|
||||
t.Errorf("NextScheduledAt parse: %v", err)
|
||||
} else if next.UTC().Hour() != 3 || next.UTC().Minute() != 0 {
|
||||
t.Errorf("NextScheduledAt time = %v, want 03:00 UTC in some day", next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminScanSchedule_Patch_WeeklyMissingDay_400(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
h, pool := testHandlers(t)
|
||||
s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{})
|
||||
h.scheduler = s
|
||||
admin := seedUser(t, pool, "schedweekly", "pw", true)
|
||||
|
||||
body := `{"mode":"weekly","time_of_day":"03:00"}`
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/admin/scan/schedule",
|
||||
bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminScheduleRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want 400; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminScanSchedule_Patch_OffNormalizes(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
h, pool := testHandlers(t)
|
||||
s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{})
|
||||
h.scheduler = s
|
||||
admin := seedUser(t, pool, "schedoff", "pw", true)
|
||||
|
||||
if _, err := pool.Exec(context.Background(),
|
||||
`UPDATE scan_schedule SET mode='daily', time_of_day='03:00' WHERE id=true`,
|
||||
); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
|
||||
body := `{"mode":"off","time_of_day":"03:00"}`
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/admin/scan/schedule",
|
||||
bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withUser(req, admin)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminScheduleRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var timeOfDay *string
|
||||
if err := pool.QueryRow(context.Background(),
|
||||
`SELECT time_of_day FROM scan_schedule WHERE id=true`,
|
||||
).Scan(&timeOfDay); err != nil {
|
||||
t.Fatalf("verify: %v", err)
|
||||
}
|
||||
if timeOfDay != nil {
|
||||
t.Errorf("time_of_day = %v, want nil after mode=off normalize", timeOfDay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminScanSchedule_NonAdmin_403(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
h, pool := testHandlers(t)
|
||||
s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{})
|
||||
h.scheduler = s
|
||||
user := seedUser(t, pool, "schednonadmin", "pw", false)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/scan/schedule", nil)
|
||||
req = withUser(req, user)
|
||||
rec := httptest.NewRecorder()
|
||||
newAdminScheduleRouter(h).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("status = %d, want 403", rec.Code)
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -26,7 +26,7 @@ import (
|
||||
// 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, lidarrCfg *lidarrconfig.Service, lidarrReqs *lidarrrequests.Service, lidarrQuar *lidarrquarantine.Service, tracksSvc *tracks.Service, playlistsSvc *playlists.Service, coverEnricher *coverart.Enricher, coverBackfillCap int, coverSettings *coverart.SettingsService, scanner *library.Scanner, scanCfg library.RunScanConfig, dataDir string) {
|
||||
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playevents.Writer, recCfg config.RecommendationConfig, lidarrCfg *lidarrconfig.Service, lidarrReqs *lidarrrequests.Service, lidarrQuar *lidarrquarantine.Service, tracksSvc *tracks.Service, playlistsSvc *playlists.Service, coverEnricher *coverart.Enricher, coverBackfillCap int, coverSettings *coverart.SettingsService, scanner *library.Scanner, scanCfg library.RunScanConfig, scheduler *library.Scheduler, dataDir string) {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
h := &handlers{
|
||||
pool: pool, logger: logger, events: events, recCfg: recCfg,
|
||||
@@ -41,6 +41,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
coverSettings: coverSettings,
|
||||
scanner: scanner,
|
||||
scanCfg: scanCfg,
|
||||
scheduler: scheduler,
|
||||
dataDir: dataDir,
|
||||
}
|
||||
|
||||
@@ -115,6 +116,8 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
|
||||
admin.Get("/scan/status", h.handleGetScanStatus)
|
||||
admin.Post("/scan/run", h.handleTriggerScan)
|
||||
admin.Get("/scan/schedule", h.handleGetScanSchedule)
|
||||
admin.Patch("/scan/schedule", h.handlePatchScanSchedule)
|
||||
|
||||
admin.Get("/library/coverage", h.handleGetLibraryCoverage)
|
||||
|
||||
@@ -152,5 +155,6 @@ type handlers struct {
|
||||
coverSettings *coverart.SettingsService
|
||||
scanner *library.Scanner
|
||||
scanCfg library.RunScanConfig
|
||||
scheduler *library.Scheduler
|
||||
dataDir string
|
||||
}
|
||||
|
||||
@@ -443,7 +443,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.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverArtBackfillCap, h.coverSettings, h.scanner, h.scanCfg, h.dataDir)
|
||||
Mount(r, h.pool, h.logger, w, config.RecommendationConfig{RadioSize: 50, RadioSizeMax: 200, RecentlyPlayedHours: 1}, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverArtBackfillCap, h.coverSettings, h.scanner, h.scanCfg, nil, h.dataDir)
|
||||
|
||||
paths := []string{
|
||||
"/api/artists",
|
||||
|
||||
@@ -77,10 +77,11 @@ type Server struct {
|
||||
CoverSettings *coverart.SettingsService
|
||||
LibraryScanner *library.Scanner
|
||||
ScanCfg library.RunScanConfig
|
||||
Scheduler *library.Scheduler
|
||||
}
|
||||
|
||||
func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger, subCfg subsonic.Config, eventsCfg config.EventsConfig, recCfg config.RecommendationConfig, dataDir string, brandingCfg config.BrandingConfig, coverEnricher *coverart.Enricher, coverArtBackfillCap int, coverSettings *coverart.SettingsService, libraryScanner *library.Scanner, scanCfg library.RunScanConfig) *Server {
|
||||
return &Server{Logger: logger, Pool: pool, Scanner: scanner, SubsonicCfg: subCfg, EventsCfg: eventsCfg, RecommendationCfg: recCfg, DataDir: dataDir, BrandingCfg: brandingCfg, CoverEnricher: coverEnricher, CoverArtBackfillCap: coverArtBackfillCap, CoverSettings: coverSettings, LibraryScanner: libraryScanner, ScanCfg: scanCfg}
|
||||
func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger, subCfg subsonic.Config, eventsCfg config.EventsConfig, recCfg config.RecommendationConfig, dataDir string, brandingCfg config.BrandingConfig, coverEnricher *coverart.Enricher, coverArtBackfillCap int, coverSettings *coverart.SettingsService, libraryScanner *library.Scanner, scanCfg library.RunScanConfig, scheduler *library.Scheduler) *Server {
|
||||
return &Server{Logger: logger, Pool: pool, Scanner: scanner, SubsonicCfg: subCfg, EventsCfg: eventsCfg, RecommendationCfg: recCfg, DataDir: dataDir, BrandingCfg: brandingCfg, CoverEnricher: coverEnricher, CoverArtBackfillCap: coverArtBackfillCap, CoverSettings: coverSettings, LibraryScanner: libraryScanner, ScanCfg: scanCfg, Scheduler: scheduler}
|
||||
}
|
||||
|
||||
func (s *Server) Router() http.Handler {
|
||||
@@ -114,7 +115,7 @@ func (s *Server) Router() http.Handler {
|
||||
lidarrQuar := lidarrquarantine.NewService(s.Pool, lidarrCfg, lidarrClientFn)
|
||||
tracksSvc := tracks.NewService(s.Pool, s.Logger, lidarrUnmonitorAdapter{fn: lidarrClientFn}, s.DataDir)
|
||||
playlistsSvc := playlists.NewService(s.Pool, s.Logger, s.DataDir)
|
||||
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverArtBackfillCap, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.DataDir)
|
||||
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverArtBackfillCap, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.Scheduler, s.DataDir)
|
||||
// /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
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil)
|
||||
ts := httptest.NewServer(s.Router())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestHealthz_IncludesMinClientVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_ServesSPAAtRoot(t *testing.T) {
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil)
|
||||
ts := httptest.NewServer(s.Router())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -92,7 +92,7 @@ func TestRouter_ServesSPAAtRoot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) {
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil)
|
||||
ts := httptest.NewServer(s.Router())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) {
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil)
|
||||
ts := httptest.NewServer(s.Router())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRouter_RestPathNotSwallowedBySPA(t *testing.T) {
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil)
|
||||
ts := httptest.NewServer(s.Router())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestRouter_AdminSubtreeNotShadowed(t *testing.T) {
|
||||
}
|
||||
|
||||
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), pool,
|
||||
stubScanner{}, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
|
||||
stubScanner{}, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil)
|
||||
ts := httptest.NewServer(s.Router())
|
||||
defer ts.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user