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:
@@ -0,0 +1,93 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/tracks"
|
||||
)
|
||||
|
||||
// removeTrackResponse is the wire shape from spec §5 (M7 #372). The three
|
||||
// optional fields are omitted unless the corresponding cleanup actually
|
||||
// happened, so the response stays minimal in the common case.
|
||||
type removeTrackResponse struct {
|
||||
DeletedTrackID string `json:"deleted_track_id"`
|
||||
DeletedAlbumID *string `json:"deleted_album_id,omitempty"`
|
||||
DeletedArtistID *string `json:"deleted_artist_id,omitempty"`
|
||||
LidarrUnmonitorFailed *bool `json:"lidarr_unmonitor_failed,omitempty"`
|
||||
}
|
||||
|
||||
// handleRemoveTrack implements DELETE /api/admin/tracks/{id}?unmonitor=true|false.
|
||||
//
|
||||
// Admin-only (gated by auth.RequireAdmin on the /admin route group). Always
|
||||
// deletes the file + DB row and runs the album/artist cascade tidy-up. When
|
||||
// unmonitor=true and the track has an mbid, also calls Lidarr.UnmonitorTrack
|
||||
// — failure there is non-fatal (the destructive part already completed) and
|
||||
// surfaces as `lidarr_unmonitor_failed: true` in the success envelope.
|
||||
//
|
||||
// Per spec §5, Lidarr-side errors during the unmonitor step do NOT map to
|
||||
// wire error codes; the only error codes this handler emits are not_found,
|
||||
// server_error, plus the auth codes the middleware emits upstream.
|
||||
func (h *handlers) handleRemoveTrack(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := chi.URLParam(r, "id")
|
||||
trackID, ok := parseUUID(idStr)
|
||||
if !ok {
|
||||
// Malformed id is functionally equivalent to "no such track" for
|
||||
// the spec's error surface — collapse both to 404 not_found so
|
||||
// the client doesn't have to handle a separate bad_request branch
|
||||
// for a code path that's only reachable via UI bugs.
|
||||
writeErr(w, http.StatusNotFound, "not_found", "track not found")
|
||||
return
|
||||
}
|
||||
|
||||
unmonitor := false
|
||||
if v := r.URL.Query().Get("unmonitor"); v != "" {
|
||||
if parsed, err := strconv.ParseBool(v); err == nil {
|
||||
unmonitor = parsed
|
||||
}
|
||||
}
|
||||
|
||||
admin, ok := auth.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
// Defensive: RequireUser+RequireAdmin should have run upstream.
|
||||
// If we got here without a user in context the routing is broken.
|
||||
writeErr(w, http.StatusUnauthorized, "unauthenticated", "no session")
|
||||
return
|
||||
}
|
||||
|
||||
deletedAlbum, deletedArtist, lidarrUnmonitorFailed, err := h.tracks.RemoveTrack(
|
||||
r.Context(), trackID, admin.ID, unmonitor,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, tracks.ErrNotFound) {
|
||||
writeErr(w, http.StatusNotFound, "not_found", "track not found")
|
||||
return
|
||||
}
|
||||
h.logger.Error("api: remove track failed", "err", err, "track_id", idStr)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "remove failed")
|
||||
return
|
||||
}
|
||||
|
||||
resp := removeTrackResponse{DeletedTrackID: idStr}
|
||||
if deletedAlbum != nil {
|
||||
s := uuidToString(*deletedAlbum)
|
||||
resp.DeletedAlbumID = &s
|
||||
}
|
||||
if deletedArtist != nil {
|
||||
s := uuidToString(*deletedArtist)
|
||||
resp.DeletedArtistID = &s
|
||||
}
|
||||
// Only emit the flag when unmonitor was requested AND it failed.
|
||||
// Avoids "false" cluttering the wire when the operator didn't ask
|
||||
// for an unmonitor in the first place.
|
||||
if unmonitor && lidarrUnmonitorFailed {
|
||||
t := true
|
||||
resp.LidarrUnmonitorFailed = &t
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
Reference in New Issue
Block a user