package api import ( "errors" "net/http" "strconv" "github.com/go-chi/chi/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "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, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "track not found"}) return } unmonitor := false if v := r.URL.Query().Get("unmonitor"); v != "" { if parsed, err := strconv.ParseBool(v); err == nil { unmonitor = parsed } } // Defensive: RequireUser+RequireAdmin should have run upstream. If // we got here without a user in context the routing is broken. admin, ok := requireUser(w, r) if !ok { 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, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "track not found"}) return } h.logger.Error("api: remove track failed", "err", err, "track_id", idStr) writeErr(w, apierror.InternalMsg("remove failed", err)) 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) }