package api import ( "errors" "net/http" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/lidarrquarantine" ) // quarantineView is the JSON shape returned by the user-facing endpoints // that operate on a single quarantine row (POST /api/quarantine). type quarantineView struct { UserID string `json:"user_id"` TrackID string `json:"track_id"` Reason string `json:"reason"` Notes *string `json:"notes,omitempty"` CreatedAt pgtype.Timestamptz `json:"created_at"` } func quarantineViewFrom(row dbq.LidarrQuarantine) quarantineView { return quarantineView{ UserID: uuidToString(row.UserID), TrackID: uuidToString(row.TrackID), Reason: string(row.Reason), Notes: row.Notes, CreatedAt: row.CreatedAt, } } // flagBody is the JSON body for POST /api/quarantine. track_id is sent as // the canonical hyphenated string the SPA already gets from /api/tracks/*. type flagBody struct { TrackID string `json:"track_id"` Reason string `json:"reason"` Notes string `json:"notes"` } // handleFlag implements POST /api/quarantine. Upserts via Service.Flag. func (h *handlers) handleFlag(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { return } var body flagBody if !decodeBody(w, r, &body) { return } trackID, ok := parseUUID(body.TrackID) if !ok { writeErr(w, apierror.BadRequest("bad_request", "invalid track id")) return } row, err := h.lidarrQuarantine.Flag(r.Context(), user.ID, trackID, body.Reason, body.Notes) if err != nil { switch { case errors.Is(err, lidarrquarantine.ErrBadReason): writeErr(w, apierror.BadRequest("bad_reason", "invalid reason")) case errors.Is(err, lidarrquarantine.ErrTrackNotFound): writeErr(w, &apierror.Error{Status: 404, Code: "track_not_found", Message: "track does not exist"}) default: h.logger.Error("api: flag", "err", err) writeErr(w, apierror.InternalMsg("flag failed", err)) } return } writeJSON(w, http.StatusCreated, quarantineViewFrom(row)) } // handleUnflag implements DELETE /api/quarantine/{track_id}. func (h *handlers) handleUnflag(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { return } id, ok := requireURLUUID(w, r, "track_id") if !ok { return } if err := h.lidarrQuarantine.Unflag(r.Context(), user.ID, id); err != nil { if errors.Is(err, lidarrquarantine.ErrQuarantineNotFound) { writeErr(w, &apierror.Error{Status: 404, Code: "quarantine_not_found", Message: "no quarantine for that track"}) return } h.logger.Error("api: unflag", "err", err) writeErr(w, apierror.InternalMsg("unflag failed", err)) return } w.WriteHeader(http.StatusNoContent) } // quarantineMineView is the per-row shape returned by GET /api/quarantine/mine, // composed from the joined ListQuarantineForUserRow. type quarantineMineView struct { TrackID string `json:"track_id"` Reason string `json:"reason"` Notes *string `json:"notes,omitempty"` CreatedAt pgtype.Timestamptz `json:"created_at"` TrackTitle string `json:"track_title"` TrackDurationMs int32 `json:"track_duration_ms"` AlbumID string `json:"album_id"` AlbumTitle string `json:"album_title"` AlbumCoverArtPath *string `json:"album_cover_art_path,omitempty"` ArtistID string `json:"artist_id"` ArtistName string `json:"artist_name"` } // handleListMyQuarantine implements GET /api/quarantine/mine. Returns the // caller's quarantines with track/album/artist detail. func (h *handlers) handleListMyQuarantine(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { return } rows, err := h.lidarrQuarantine.ListMine(r.Context(), user.ID) if err != nil { h.logger.Error("api: list mine", "err", err) writeErr(w, apierror.InternalMsg("list failed", err)) return } out := make([]quarantineMineView, 0, len(rows)) for _, row := range rows { out = append(out, quarantineMineView{ TrackID: uuidToString(row.LidarrQuarantine.TrackID), Reason: string(row.LidarrQuarantine.Reason), Notes: row.LidarrQuarantine.Notes, CreatedAt: row.LidarrQuarantine.CreatedAt, TrackTitle: row.TrackTitle, TrackDurationMs: row.TrackDurationMs, AlbumID: uuidToString(row.AlbumID), AlbumTitle: row.AlbumTitle, AlbumCoverArtPath: row.AlbumCoverArtPath, ArtistID: uuidToString(row.ArtistID), ArtistName: row.ArtistName, }) } writeJSON(w, http.StatusOK, out) }