feat(api): DELETE /api/admin/tracks/{id} for M7 #372

Admin-only handler. Calls tracks.RemoveTrack with the unmonitor flag
parsed from the query string. ErrNotFound -> 404; everything else
unexpected -> 500. Lidarr-side failures during unmonitor flow as
lidarr_unmonitor_failed: true in the success envelope (non-fatal —
the file + DB delete already completed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 22:41:51 -04:00
parent f6975cfad3
commit bb931746e3
5 changed files with 440 additions and 3 deletions
+25 -1
View File
@@ -3,6 +3,7 @@ package server
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strings"
@@ -23,9 +24,31 @@ import (
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrrequests"
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
"git.fabledsword.com/bvandeusen/minstrel/internal/tracks"
"git.fabledsword.com/bvandeusen/minstrel/web"
)
// lidarrUnmonitorAdapter wires the per-call lidarrClientFn factory pattern
// (used elsewhere in this package so admin Lidarr config edits take effect
// without restart) into the tracks.LidarrUnmonitorer interface that
// internal/tracks expects. When the factory returns nil (Lidarr disabled)
// we surface a sentinel error — tracks.Service treats UnmonitorTrack
// failures as non-fatal, so this collapses to a `lidarr_unmonitor_failed`
// flag in the response and the destructive part still runs.
type lidarrUnmonitorAdapter struct {
fn func() *lidarr.Client
}
var errLidarrDisabled = errors.New("lidarr disabled")
func (a lidarrUnmonitorAdapter) UnmonitorTrack(ctx context.Context, trackMbid, albumMbid string) error {
c := a.fn()
if c == nil {
return errLidarrDisabled
}
return c.UnmonitorTrack(ctx, trackMbid, albumMbid)
}
// ScanTrigger is the subset of the scanner the HTTP handler needs. Kept as an
// interface so tests can stub it without touching the DB.
type ScanTrigger interface {
@@ -74,7 +97,8 @@ func (s *Server) Router() http.Handler {
}
lidarrReqs := lidarrrequests.NewService(s.Pool, lidarrCfg, lidarrClientFn, nil)
lidarrQuar := lidarrquarantine.NewService(s.Pool, lidarrCfg, lidarrClientFn)
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar)
tracksSvc := tracks.NewService(s.Pool, s.Logger, lidarrUnmonitorAdapter{fn: lidarrClientFn})
api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc)
// /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