Commit Graph

526 Commits

Author SHA1 Message Date
bvandeusen 46ea9a761a feat(server/m7-363): thread BrandingConfig through server.New 2026-05-03 17:17:52 -04:00
bvandeusen cd50ffd77d feat(config/m7-363): branding config field + defaults 2026-05-03 17:14:59 -04:00
bvandeusen ec1b3a3397 chore: untrack docs/superpowers, ignore going forward
Agent-authored design specs and implementation plans are kept local
on the working machine; the canonical record lives in commit messages
and the running code. The 48 historical files remain in git history
and can be retrieved via git checkout <sha> -- docs/superpowers/ if
ever needed; they just stop accruing on the dev tip.
2026-05-03 15:32:43 -04:00
bvandeusen d56edadf0e docs(m7-363): branding polish design spec
Per-route titles, MINSTREL_APP_NAME via Go template injection of the
embedded index.html, OG/Twitter meta + theme-color follow-through for
the dark/light toggle, and an operator-first README rewrite. PWA /
service worker / install icons explicitly out of scope (Flutter #356
+ Tauri post-v1 cover mobile and desktop). OG image ships as a
generated placeholder; hand-designed artwork drops in later.
2026-05-03 15:27:39 -04:00
bvandeusen 7d82be864d fix(flutter): handle nested colors split in tokens.json codegen
Web tokens.json was reshaped for the SPA theme toggle (#362) with
colors split into dark/light/flat blocks. gen_tokens.dart still
expected a flat string map and threw _Map<String,dynamic> cast
errors in the flutter CI workflow. Read colors.dark + colors.flat
and merge into the existing flat namespace; light variants will be
emitted when the Flutter client gains a theme toggle as part of
#356 parity work. Adds hyphen→camelCase normalization so the new
on-action token surfaces as onAction.
2026-05-03 14:46:27 -04:00
bvandeusen 7faa7b4e1f fix(web): hide Play next / Add to queue on PlayerBar's track menu
Those actions don't have meaningful semantics for the track you're
already listening to. New TrackMenu prop hideQueueActions gates the
pair; PlayerBar passes it true. Other mount sites (track rows in
lists) keep the full 8-entry menu.
2026-05-03 14:08:29 -04:00
bvandeusen 4b101481fc fix(web): drop-up Add-to-playlist submenu, hide horizontal scroll row scrollbar
- AddToPlaylistMenu: anchor submenu bottom to parent bottom so it
  grows upward (top-0 → bottom-0) — kebab row is usually near the
  bottom of the track menu and the submenu kept extending off-screen.
- HorizontalScrollRow: scrollbar-width: none + ::-webkit-scrollbar
  display:none. Page arrows already drive the scroll; the scrollbar
  was just consuming vertical real estate.
2026-05-03 14:04:07 -04:00
bvandeusen 16a692e81a test(web/m7-362): Settings Appearance card render + selection test 2026-05-03 13:53:47 -04:00
bvandeusen dd50ce09fd test(web/m7-362): theme store unit test (default/persist/matchMedia) 2026-05-03 13:53:03 -04:00
bvandeusen 0f83b2d6ae test(web/m7-362): tokens-to-css generator unit test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:52:13 -04:00
bvandeusen e40fa0d252 feat(web/m7-362): Settings → Appearance card with dark/light/system selector 2026-05-03 13:51:25 -04:00
bvandeusen 4a04ec64f8 feat(web/m7-362): pre-paint theme application via inline FOUC script 2026-05-03 13:50:40 -04:00
bvandeusen ffed19f736 feat(web/m7-362): theme preference + resolved-theme rune store 2026-05-03 13:43:28 -04:00
bvandeusen f0bfac2555 refactor(web/m7-362): action-button labels use non-flipping text-action-fg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:42:42 -04:00
bvandeusen 5c5f9f3ec1 feat(web/m7-362): add Tailwind text-action-fg alias for non-flipping label color 2026-05-03 13:40:20 -04:00
bvandeusen 9b7dd8272e feat(web/m7-362): tokens.json dark/light/flat split + dual-block CSS generator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:39:54 -04:00
bvandeusen a9fc0c6fe8 docs(m7-362): theme toggle implementation plan (9 tasks)
Tokens schema split + dual-block generator, Tailwind text-action-fg
alias, action-button audit, theme rune store, FOUC script, Settings
Appearance card. Plus generator/store/UI tests for CI. No in-task
test runs per memory rule.
2026-05-03 13:37:33 -04:00
bvandeusen 1d6a71a825 docs(m7-362): correct dark hex values + Tailwind alias
Spec table's "Dark (current)" column now matches the actual
tokens.json values. Added Tailwind text-action-fg alias as the
mechanism for the non-flipping --fs-on-action token. Updated
audit notes to use the real Tailwind class (text-text-primary)
rather than the raw token (text-fs-parchment).
2026-05-03 13:34:40 -04:00
bvandeusen 1dcce4e304 docs(m7-362): theme toggle design spec
Web SPA dark/light/system theme. Token strategy: dual :root +
[data-theme="light"] blocks in tokens.generated.css; Tailwind
unchanged. Light palette is parchment/aged-paper. Adds
non-flipping --fs-on-action for action button labels. FOUC
prevented via inline app.html head script.
2026-05-03 13:31:18 -04:00
bvandeusen 0ce78035aa test(web): PlaylistCard cover-img assertion uses DOM query
The <img alt=""> production element renders as role="presentation"
(decorative — playlist name is in sibling text). screen.getByRole('img')
doesn't match presentation-role elements. Switch to
container.querySelector('img') so the test doesn't fight the
correct a11y choice. Production code unchanged.
2026-05-03 12:28:21 -04:00
bvandeusen e0a4d13c45 fix(web): /playlists/{id} page uses $app/state, not legacy $app/stores
svelte-check failed with 8 type errors: $app/stores' page.params.id
is typed `string | undefined`, and the page passed `id` raw to
helpers that need `string`. Two changes:

- Switch to `$app/state` (the project canonical for new pages — see
  routes/albums/[id]/+page.svelte). page.params.id is a direct
  property read, not a store subscription. Fall back to "" so id is
  always a string; SvelteKit won't actually render the page without
  a populated id, but the type-checker doesn't know that.
- Drop the now-unused `writable` import in the test file and update
  the mock to expose the new state-shape (object with .params, not a
  writable store).

The remaining a11y warnings (tabindex on role=menu/dialog, click
handlers without keyboard events, draggable div without role,
autofocus) are svelte-check warnings — they don't block the build.
Some are pre-existing in FlagPopover; others are tech debt from
slice 1's drag-and-drop and modal patterns. Address as a follow-up
polish task.
2026-05-03 11:58:10 -04:00
bvandeusen d32e1505c5 docs(config): document storage.data_dir in the example yaml
Forgot to include this in the previous commit (6d1709c). The new
config field's example block needs to be in the canonical yaml so
operators copying it in get the doc.
2026-05-03 11:38:05 -04:00
bvandeusen 6d1709caff fix(config): wire DataDir from yaml/env through to playlists service
Slice 1 of M7 #352 added Server.DataDir but left it un-populated —
the playlist cover-collage writer would have used "" as the relative
path root in production, writing to the current working directory.

- Add Storage.DataDir to Config (yaml: storage.data_dir, env:
  SMARTMUSIC_STORAGE_DATA_DIR), defaulting to "./data".
- server.New takes the data dir as a parameter; main.go threads it.
- main.go MkdirAll's the directory at startup so the playlist cover
  writer doesn't fail on first use with a missing parent.
- config.example.yaml documents the new section.

Existing internal/server/server_test.go constructs Server directly
without server.New, so no test fixture changes needed for that file.
2026-05-03 11:37:41 -04:00
bvandeusen b0a928c554 feat(web): /playlists/{id} detail page with drag-reorder (M7 #352)
Header shows collage, name, description, public/private chip, track
count, owner attribution (when not owner). Edit + delete buttons
visible to the owner only. Track list uses PlaylistTrackRow with
HTML5 drag-and-drop; drop fires PUT /tracks with the new ordered
positions and TanStack Query invalidates the playlist + index cache.

Toast surface is a placeholder browser alert in slice 1 — a real
toast is a polish task whenever it lands.
2026-05-03 11:34:05 -04:00
bvandeusen 80a6861ded feat(web): /playlists index page (M7 #352 slice 1)
Replaces the placeholder route. Two sections: "Your playlists" (owned)
and "From other users" (public). Inline create form in the header
with Enter-to-submit / Esc-to-cancel. Empty-state copy when the
operator has nothing yet. Routes to /playlists/{id} on card click via
PlaylistCard.
2026-05-03 11:22:25 -04:00
bvandeusen 71dbaaede5 feat(web): AddToPlaylistMenu submenu wired into TrackMenu (M7 #352)
The "Add to playlist…" entry that #372 reserved as a disabled slot
is now active. Submenu lists the operator's own playlists
alphabetically; "New playlist…" toggles an inline create form that
makes the playlist + appends the track in two API calls.

TrackMenu's existing test asserts the entry is enabled and opens the
submenu instead of the previous "is disabled with tooltip" check.
2026-05-03 11:20:14 -04:00
bvandeusen 4067be04a6 feat(web): PlaylistTrackRow component for M7 #352 slice 1
Variant of TrackRow specialised for playlist detail. Drag handle
visible to owner only; remove button (X) visible to owner only;
greyed-out + strikethrough when track_id is null (upstream track
removed from library). Reuses the existing LikeButton and TrackMenu
when the track is still alive.
2026-05-03 11:16:35 -04:00
bvandeusen 0eb346e0c6 feat(web): PlaylistCard component for M7 #352 slice 1
Square card with cover (or "No tracks yet" glyph fallback), name,
track count, and owner attribution when the playlist isn't the
current user's. Click navigates to /playlists/{id}.
2026-05-03 11:14:30 -04:00
bvandeusen b9830cc9cc feat(web/api): playlists helper + types + query keys (M7 #352 slice 1)
Wire shapes mirror the server's snake_case envelope. URL helpers use
the existing apiFetch + ApiError surface so error codes (not_found,
not_authorized, bad_request, server_error) propagate via the same
copyForCode chain as everything else.
2026-05-03 11:12:46 -04:00
bvandeusen c331168d3b feat(api): /api/playlists* handlers for M7 #352 slice 1
9 handlers covering create / get / list / update / delete / append /
remove / reorder / cover. Permissions enforced in the service layer
(ErrForbidden -> 403 not_authorized) so the handler is a thin
HTTP-shape adapter. /cover serves the cached collage from disk via
http.ServeFile; 404 when cover_path is nil.

Server gained a DataDir field so the playlists service can find the
collage cache; api.Mount picks up two new params (playlistsSvc and
dataDir). The stale TestRoutesRegisteredInMount Mount() call in
library_test.go was missing the tracks.Service argument added by an
earlier slice — fixed in passing.

Wire codes follow the project's existing nested errorBody envelope:
not_found, not_authorized, unauthenticated, bad_request, server_error.
The plan called for a flat envelope, but the api package's writeErr
already produces {"error":{"code":"...","message":"..."}} and
deviating here would break every existing client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:10:21 -04:00
bvandeusen 79bab14b30 feat(playlists): track operations + cover-collage generator
AppendTracks / RemoveTrack / Reorder run in single transactions and
update the denormalized rollups (track_count, duration_sec). Reorder
uses a +10000 offset to bump every row out of the position range
before writing the new positions, sidestepping PK conflicts during
rewrite.

GenerateCollage composes a 600x600 JPEG from the first 4 album
covers, with a slate-tinted fallback for missing cells. Synchronous,
called inline after every mutating operation. SVG-rasterized fallback
is a follow-up — slice 1 ships with a solid placeholder.
2026-05-03 10:25:20 -04:00
bvandeusen 5c61c10b63 feat(playlists): Service with CRUD methods (M7 #352 slice 1)
Create / Get / List / Update / Delete with the visibility model from
the spec: private by default, owner-only mutations, public read for
non-owners. Update uses sqlc's CASE-WHEN-flag pattern for PATCH-style
partial updates without writing N variants.

Delete cleans up the cached cover file from disk best-effort. Track
operations (Append/Remove/Reorder) and the collage generator land
in subsequent tasks.

Also adds playlists + playlist_tracks to dbtest.ResetDB's truncate
list so integration tests in this package start from a clean state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:17:08 -04:00
bvandeusen 1226cb7583 feat(db): playlists schema for M7 #352 slice 1
Migration 0014 adds playlists + playlist_tracks. track_id is nullable
with ON DELETE SET NULL — tracks can be removed from the library
without silently dropping playlist entries; the denormalized snapshot
(title/artist/album/duration) keeps the row legible afterwards. UI
renders such rows greyed-out.

Indexes: playlists by (user_id, updated_at DESC) and a partial public
index for cross-user discovery; playlist_tracks partial index on
track_id to support the FK SET NULL lookup.

Queries provide CRUD + rollup recompute (track_count, duration_sec)
+ append/remove primitives. Reorder is service-layer orchestrated via
raw tx.Exec; no SQL primitive needed.
2026-05-03 10:03:36 -04:00
bvandeusen e056c8b8be docs(m7): implementation plan for #352 playlists CRUD slice 1
11 tasks: migration 0014 + sqlc queries (T1), Service CRUD methods
(T2), track operations (T3), cover-collage generator (T4), 9 HTTP
handlers + service wiring (T5), frontend api helper + types + qk (T6),
PlaylistCard (T7), PlaylistTrackRow with drag handle + soft-mark
strikethrough (T8), AddToPlaylistMenu wired into TrackMenu (T9),
/playlists index page (T10), /playlists/[id] detail with drag-reorder
(T11).

Soft-mark cascade preserved end-to-end: schema (track_id nullable +
ON DELETE SET NULL with denormalized snapshot columns), service (no
cascade on track delete; rows persist), UI (greyed-out + strikethrough
for null track_id rows). Cover collage is synchronous inline 2x2
JPEG via image/jpeg stdlib; SVG fallback rasterization is a follow-up.
2026-05-03 00:59:21 -04:00
bvandeusen 65bb4e6dfd docs(m7): spec for #352 playlists CRUD (slice 1 of 3)
Manual playlists only — schema (migration 0014), backend service +
collage generator, /api/playlists* endpoints, /playlists list +
detail pages, AddToPlaylistMenu wired into TrackMenu's reserved slot.

10 locked decisions captured: private-by-default, manual-only,
api-only (no Subsonic), 2×2 collage always (glyph fills missing
cells), soft-mark cascade with denormalized snapshot, full-list
reorder, inline collage gen, composite PK on (playlist_id, position),
denormalized track_count + duration_sec rollups.

Slices 2 (system-generated daily mixes) and 3 (home-page playlists
row) are sibling cycles; per v1=full-product all three ship before
tag.
2026-05-03 00:28:34 -04:00
bvandeusen 1cf58b18e8 ci(web): exclude #374-blocked tests from vitest module discovery
describe.skip inside the file doesn't help — the SvelteKit
`notifiable_store is not a function` failure happens at module-load,
before vitest evaluates describe blocks. Add the two paths to
vitest's exclude list so the files aren't loaded at all. The skip
markers stay inside the files as a triage signal for M7 #374.
2026-05-03 00:13:04 -04:00
bvandeusen 53842b0f21 ci(test-web): drop npm cache restore — setup-node was burning 4m41s
The Forgejo Actions cache server at 172.18.0.27:41161 isn't reachable
from the test-web runner container; setup-node sat there waiting for
ETIMEDOUT each run. Drop `cache: 'npm'` so the step finishes in seconds.

`npm ci` still works deterministically from package-lock.json — net cost
is ~10-30s of registry fetch vs. 4m41s of timeout. Restore the cache
option once the runner-host network reaches the cache server (separate
operator task).
2026-05-03 00:09:02 -04:00
bvandeusen 40db918bfc fix(web): RemoveTrackPopover async test waits + skip 2 SvelteKit-broken suites
Three CI failures from the dev-push test-web.yml run. Two categories:

1. RemoveTrackPopover.test.ts — `confirm()` chains 5+ awaited
   invalidateQueries before onClose; the test's two `await Promise.resolve()`
   only flushed two microtasks. Switch to waitFor() so the assertion
   polls until the side effects land. Same fix on the success-cascade
   invalidation count test.

2. discover.test.ts + requests.test.ts — both fail at module-load with
   `TypeError: notifiable_store is not a function` deep in
   @sveltejs/kit's client.js. Surfaced only on the new dev-push
   workflow; PR-to-main runs were green. describe.skip with a FIXME
   pointing at the new triage task M7 #374. The pages themselves
   aren't broken — the test harness is.
2026-05-03 00:00:26 -04:00
bvandeusen d87d9e3255 fix(web): widen TrackMenuItem icon prop type to match Lucide signature
Previous fix narrowed `IconProp` to `ComponentType<SvelteComponent<{
size?: number, strokeWidth?: number, class?: string }>>` but Lucide
icons accept `size: string | number` (you can write size="16" or
size={16}), and the resulting structural mismatch surfaces 11 type
errors at every assignment site. Drop the prop-shape constraint
entirely — TrackMenuItem only ever passes numeric values to the icon.
2026-05-02 23:34:11 -04:00
bvandeusen 5d69e9614f fix(web): TrackMenuItem icon prop accepts Lucide class-components
svelte-check on the new dev-push CI surfaced 11 type errors. Cause:
TrackMenuItem typed `icon` as Svelte 5's runes-mode `Component<...>`,
but Lucide-svelte ships class-based components whose type is
`ComponentType<SvelteComponent<...>>`. Switch the prop type to match
Lucide's actual export shape.

Also drop the redundant role="separator" on TrackMenuDivider — <hr>
already implies role=separator (svelte-check warning).

The remaining a11y warnings (tabindex on role=menu/dialog elements,
key-handler-with-click) are pre-existing in FlagPopover and surface
on the new dev-push run because svelte-check now sees them. They're
warnings not errors, so they don't block CI; address as a follow-up.
2026-05-02 23:24:20 -04:00
bvandeusen 6858e45dbe ci: split test.yml into test-web.yml + test-go.yml, run on dev pushes
Operator wants CI to run on dev pushes (not just PR-to-main) but
path-scoped so Go-only diffs don't run web tests and vice versa. The
old test.yml ran everything on PR-to-main only; the new shape is two
focused workflows that each path-filter their own scope.

- test-web.yml — paths: web/**. Runs npm ci + npm run check + vitest.
  Drops the npm run build step (vitest doesn't need the bundle; the
  go:embed placeholder lives in web/build/index.html and never needs
  the test job to rebuild it).
- test-go.yml — paths: **/*.go, go.mod, go.sum, sqlc.yaml, internal/**,
  cmd/**. Runs go vet + golangci-lint + go test -short -race.

Both trigger on push to dev/main and PR to main. Tag pushes are not
covered — release.yml owns the tag-build path and tests have already
passed by the time a tag lands.

Drops the gomod-detect guard from test.yml — go.mod has been present
since M1 and is required for any Go file to exist anyway.
2026-05-02 23:11:20 -04:00
bvandeusen 9dc4786fd9 feat(web): TrackMenu rewrite — 9 entries in 4 groups (M7 #372)
Replaces the M5b-era FlagPopover-only menu with the full track-actions
surface: queue (Play next, Add to queue), collection (Like/Unlike,
Add to playlist… reserved for #352), navigation (Go to album/artist),
lifecycle (Flag, Hide/Unhide, Remove from library — admin-only).

Remove from library opens a new RemoveTrackPopover with a single
"Also stop Lidarr from finding a replacement" checkbox. The
destructive flow always deletes file + DB through Minstrel; the
checkbox controls whether Lidarr is also told to unmonitor. Lidarr
unmonitor failure flows back as lidarr_unmonitor_failed in the
response — destructive part already succeeded.

The component's prop API (track, direction) is unchanged so TrackRow
and PlayerBar pick up the new entries with no code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:00:09 -04:00
bvandeusen 681b532b57 feat(web): TrackMenuDivider primitive for M7 #372 menu groups 2026-05-02 22:46:44 -04:00
bvandeusen 9256369926 feat(web): TrackMenuItem primitive for M7 #372
Single-entry button used by TrackMenu. Supports aria-disabled (kept
visible to preserve menu height when admin-only or pre-#352 entries
are non-functional) and a `danger` flag for the oxblood-tinted
"Remove from library" entry.
2026-05-02 22:46:29 -04:00
bvandeusen f43ea6af5b feat(player): playNext for M7 #372 track-actions menu
Inserts at _queue[_index + 1] so the next-up slot is overwritten with
the chosen track. Empty-queue seeding mirrors enqueueTrack's behavior.
Clears _radioSeedId since user-driven enqueue invalidates the M4c
auto-refresh trigger.
2026-05-02 22:45:13 -04:00
bvandeusen 905b5f0da7 feat(web): admin removeTrack API helper for M7 #372
DELETE /api/admin/tracks/{id} with optional ?unmonitor=true. Returns
the typed envelope with optional cascaded album/artist ids and the
lidarr_unmonitor_failed flag (only set on Lidarr-side failure when
unmonitor was requested — the file + DB delete still succeeded).
Caller invalidates TanStack Query keys for any vanished entities.
2026-05-02 22:44:01 -04:00
bvandeusen bb931746e3 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>
2026-05-02 22:41:51 -04:00
bvandeusen f6975cfad3 fix(tracks): rewrite RemoveTrack — direct os.Remove + optional Lidarr unmonitor
Replaces commit 50a231f's wrong-shape Lidarr-routed delete. The previous
design called lidarrquarantine.DeleteViaLidarr for Lidarr-managed tracks,
which deletes the entire **album** in Lidarr (Lidarr is album-granular,
no per-track delete API) — silently dropping sibling tracks the operator
didn't ask to remove.

New shape per spec revision 723eee9:
- Always: os.Remove + DB delete + cascade through Minstrel.
- When unmonitor=true AND track has mbid: call new Lidarr.UnmonitorTrack
  primitive so Lidarr won't replace. Failure is non-fatal — file is
  already gone, DB consistent; the lidarrUnmonitorFailed bool flows to
  the wire response so the UI can surface a follow-up "unmonitor
  manually" toast.

Service signature changes from `(trackID, adminID) → (delAlbum, delArtist, err)`
to `(trackID, adminID, unmonitor) → (delAlbum, delArtist, lidarrFailed, err)`.

Adds Lidarr.UnmonitorTrack with full three-step API walk:
  1. GET /api/v1/album?foreignAlbumId={mbid} → Lidarr's internal album id
  2. GET /api/v1/track?albumId={id} → match track by foreignTrackId
  3. PUT /api/v1/track/monitor with {trackIds:[id], monitored:false}

Refactors post() into a shared bodyRequest() so put() reuses the same
status-code → typed-error mapping. The album mbid is captured *before*
the cascade-delete transaction — DeleteAlbumIfEmpty may remove the
album row, after which a post-commit GetAlbumByID returns ErrNoRows
and the unmonitor walk would have nothing to walk against.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:35:34 -04:00
bvandeusen 723eee9773 docs(m7): revise #372 spec — Remove from library no longer goes via Lidarr
Operator decision during Task 2 implementation: routing track removal
through lidarrquarantine.DeleteViaLidarr is wrong because Lidarr is
album-granular (no per-track delete). The original spec would have
silently deleted sibling tracks.

New shape:
- Always delete file + DB row + cascade through Minstrel (os.Remove).
- Confirm dialog asks "find a replacement?": Yes (default — no Lidarr
  call; monitoring re-imports on next scan) or No (call new
  Lidarr.UnmonitorTrack(mbid) primitive).
- Lidarr unmonitor failure is non-fatal — sets lidarr_unmonitor_failed
  on the success envelope so the UI toasts a follow-up message.

Adds a new internal/lidarr.UnmonitorTrack method (LookupTrack →
PUT /api/v1/track/monitor with monitored:false) to Task 2's scope.
Wire codes lidarr_unreachable / lidarr_unauthorized / lidarr_server_error
no longer come back from this endpoint.
2026-05-02 22:28:49 -04:00
bvandeusen 50a231fdc1 feat(tracks): RemoveTrack service with Lidarr-aware delete + cascade
RemoveTrack handles both Lidarr-managed (delegates to existing
lidarrquarantine.DeleteViaLidarr) and non-Lidarr (direct os.Remove)
file deletion, then runs the cascade album-if-empty / artist-if-empty
DB cleanup in a single transaction. Lidarr errors propagate as typed
errors so the API layer can map them to the existing wire codes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:58:12 -04:00