# M5b — Quarantine workflow + admin resolution UI > **Status:** Draft for review · 2026-04-30 > > **Sub-plan of:** M5 (Lidarr integration + quarantine workflow). Decomposition recap from the M5a spec: > > - **M5a** — Lidarr connection + search/add + admin shell. Shipped on `dev`. > - **M5b (this spec)** — Quarantine workflow (per-user soft-hide of tracks, admin resolution UI). > - **M5c** — Radio "suggested additions" (out-of-library MBIDs surfaced in `/api/radio` responses; SPA add affordance). > > Each ships as its own PR with its own brainstorm/spec/plan cycle. ## 1. Goal Authenticated users can flag any local track as broken with a reason and optional notes. A flagged track disappears from that user's library, search, browse, and `/api/radio` responses — but appears on a dedicated `/library/hidden` page where they can review and un-hide. Admins see an aggregated queue at `/admin/quarantine` (one row per track with reason distribution + per-user details + inline playback) and resolve each row by clearing the reports, deleting the local file, or telling Lidarr to remove the parent album with import-list exclusion. The dominant user mental model is **data quality**: "this track is a bad rip / wrong file / wrong tags / duplicate." Personal preference framing is out of scope. ## 2. Goals and non-goals ### Goals - Authenticated user can flag any track with reason ∈ {`bad_rip`, `wrong_file`, `wrong_tags`, `duplicate`, `other`} plus optional notes. - Trigger affordance is a `` overflow (kebab) on every track row and on the now-playing player bar — sibling to `` but one-click-removed to prevent miss-clicks. - Quarantined tracks are hidden from that user's `/api/albums/:id`, `/api/artists/:id`, library track lists, search, `/api/radio`, and contextual radio. They remain visible only on `/library/hidden`. - `/library/hidden` page lists the user's own quarantines with un-hide affordance. - Admin queue at `/admin/quarantine` aggregates by track, shows reason distribution + count, expandable per-user reports, inline playback, and the three resolution actions. - Admin resolution actions: **Resolve** (clear the row), **Delete file** (rm file from disk + tracks row + clear), **Delete via Lidarr** (call Lidarr `DELETE /api/v1/album/{id}?deleteFiles=true&addImportListExclusion=true`, cascade Minstrel rows, clear). - Admin actions write to a `lidarr_quarantine_actions` audit log with snapshot fields so the log stays readable after the underlying tracks/albums are deleted. - Subsonic API (`/rest/*`) stays unfiltered — quarantine is `/api/*`-only. ### Non-goals (this slice) - Per-album / per-artist quarantine. Tracks only. - Quarantine on shared playlists, queues, or Subsonic clients. - Auto-resolve rules ("if 3 users flag, auto-quarantine globally"). Admin always decides. - Notifying users when their report is acted on (toast / email). → polish slot. - Bulk admin operations (multi-select + apply). → revisit if queue grows beyond what one-by-one handles. ## 3. Architecture ### New Go packages - **`internal/lidarrquarantine/`** — `Service` owning the quarantine lifecycle: - `Flag(ctx, userID, trackID, reason, notes)` — upsert a `lidarr_quarantine` row. - `Unflag(ctx, userID, trackID)` — remove the caller's row. - `ListMine(ctx, userID)` — caller's own quarantines (drives `/library/hidden`). - `ListAdminQueue(ctx)` — aggregated by track. Returns `[]AdminQueueRow{TrackID, TrackTitle, ArtistName, AlbumTitle, AlbumID, LidarrAlbumMBID, ReportCount, ReasonCounts, LatestAt, Reports []UserReport}`. - `Resolve(ctx, trackID, adminID)` — delete all per-user rows for the track + write audit row. - `DeleteFile(ctx, trackID, adminID)` — call `library.DeleteTrackFile` then Resolve. - `DeleteViaLidarr(ctx, trackID, adminID)` — look up the parent album in Lidarr by MBID, call `Client.DeleteAlbum(id, deleteFiles=true, addImportListExclusion=true)`, remove Minstrel rows for all tracks of that album, clear all related quarantine rows, write audit row. ### Lidarr client additions (`internal/lidarr`) - `LookupArtistByMBID(ctx, mbid) (LidarrArtist, error)` — `GET /api/v1/artist?mbId=…`. - `LookupAlbumByMBID(ctx, mbid) (LidarrAlbum, error)` — `GET /api/v1/album?foreignAlbumId=…`. - `DeleteAlbum(ctx, lidarrAlbumID, deleteFiles bool, addImportListExclusion bool) error` — `DELETE /api/v1/album/{id}?deleteFiles=...&addImportListExclusion=...`. M5b admin path always passes both `true`. Returns the same typed errors the existing client uses: `ErrUnreachable`, `ErrAuthFailed`, `ErrLookupFailed`, plus `ErrNotFound` for the lookup methods when no row matches. ### Library deletion (`internal/library`) - New `DeleteTrackFile(ctx, trackID) error` — removes the file from disk and the row from `tracks`. Album/artist rows stay (other tracks may reference them). The reconciler's MBID lookup will simply find no match next pass; M5a's reconcile is unaffected. ### Soft-hide enforcement Every endpoint that returns track lists in user-context joins against `lidarr_quarantine`: ```sql WHERE NOT EXISTS ( SELECT 1 FROM lidarr_quarantine q WHERE q.user_id = $userID AND q.track_id = tracks.id ) ``` Affected queries (modified, not new): - `GetAlbumDetail` (track list filter) - `GetArtistDetail` / library artist views (transitively, via track filter) - `Search` (track facet) - Library track-list endpoints - Radio generator + contextual radio endpoints + similarity-driven autoplay `/rest/*` Subsonic queries are NOT modified — Subsonic clients see the unfiltered library. ### Wiring - `cmd/minstrel/main.go` constructs `lidarrquarantine.Service` and injects into `internal/api/handlers`. No new background worker. - New handler files: `internal/api/quarantine.go` (user-facing), `internal/api/admin_quarantine.go` (admin endpoints). - Reuses M5a's `RequireAdmin` middleware — `/api/admin/quarantine/*` mounts on the existing admin route group. ### SPA additions - `` Svelte component — kebab icon button + dropdown menu. Renders a single "Flag this track…" item for M5b; designed for future actions. - `` Svelte component — opens from `` with the reason ``: "Bad rip", "Wrong file", "Wrong tags", "Duplicate", "Other". - Notes `