player: setQueueFromTracks fast-starts a single source at player-index 0
while the full queue is broadcast, so the transient currentIndexStream→0
emission clobbered the correct mediaItem with queue.value[0] (the first
track). Mini bar / playlist marker pinned to the wrong track until a
later index event (~the "passive ~30s recovery"). Track a logical-index
base so the player→queue mapping stays correct during the fill window;
also fixes the latent forward-fill auto-advance off-by-base.
lidarr #50: approving no longer fails when Lidarr is down. Approve
records the decision durably first, then best-effort adds; the
reconciler idempotently (re)sends unconfirmed adds every tick until they
stick (new additive lidarr_add_confirmed_at; AddArtist/AddAlbum map
Lidarr's "already exists" 400 → ErrAlreadyExists). No failed-state or
expiry by design — Lidarr keeps trying, operator monitors.
lidarr #51: Create() is now idempotent — a non-terminal request for the
same MBID returns the existing row instead of inserting a duplicate.
Rewrites the obsolete LidarrUnreachable_503 test to assert the durable-
approve contract; threads a client factory into NewReconciler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The 'lidarr_rejected' the operator was hitting on Approve was Lidarr 4xx-ing
the POST with two field-validation errors:
'Metadata Profile Id' must be greater than '0'.
'Artist Name' must not be empty.
Our payload was missing both. Lidarr's metadata profile is a separate
concept from quality profile (it controls which release types are
tracked: albums, singles, EPs, etc.) and is required by /api/v1/artist
and /api/v1/album. The 'already exists' line in the toast copy was an
incorrect guess at the cause; the real issue was an invalid payload.
Changes:
- internal/lidarr/types.go: AddArtistParams + AddAlbumParams gain
ArtistName and MetadataProfileID. New MetadataProfile struct mirroring
QualityProfile.
- internal/lidarr/client.go: AddArtist + AddAlbum payloads include both
new fields. New ListMetadataProfiles fetches GET /api/v1/metadataprofile.
- internal/lidarrrequests/service.go: Approve fetches the metadata
profile list and uses the first as a default (Lidarr installs ship
'Standard' at id=1 OOB, so this works for vanilla setups). Picker
on /admin/integrations is a follow-up. ArtistName flows from the
request row.
- web/src/routes/admin/requests/+page.svelte: drop the
speculative 'usually means already in library' copy on lidarr_rejected;
point operators at server logs for the field-level reason instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "I can't approve a request, the toast just says unknown" bug was
four problems compounded:
1. Lidarr config let enabled=true save with default_quality_profile_id=0
and default_root_folder_path=''. Approve then sent invalid POST bodies
to Lidarr, which 5xx'd.
2. lidarr.client.post() discarded Lidarr's response body on error, so
we couldn't tell why Lidarr 5xx'd from server logs.
3. handleApproveRequest's error switch didn't map ErrServerError or
ErrLookupFailed — both fell through to a generic 500 server_error.
4. apiFetch only parsed {error: {code, message}} envelopes, but admin
endpoints write {error: 'code_string'}. Every admin error toast
rendered as 'unknown'.
Fixes:
- internal/lidarr/client.go: capture up to 512 bytes of Lidarr's response
body when it returns 4xx/5xx; include in the wrapped error so server
logs show what Lidarr actually said instead of just the status bucket.
- internal/lidarrrequests/service.go: new ErrDefaultsIncomplete fires
before the Lidarr call when QP=0 or root_folder=''. Stops the bad
POST entirely.
- internal/api/admin_requests.go: handleApproveRequest now maps
ErrDefaultsIncomplete -> 'lidarr_defaults_incomplete' (400),
ErrServerError -> 'lidarr_server_error' (502),
ErrLookupFailed -> 'lidarr_rejected' (502).
- internal/api/admin_lidarr.go: handlePutLidarrConfig now requires
QP + root folder to be set whenever enabled=true.
- web/src/lib/api/client.ts: apiFetch handles both error envelope shapes
so admin error codes propagate to toasts.
- web/src/routes/admin/integrations/+page.svelte: auto-default to the
first quality profile and first root folder Lidarr returns when the
operator hasn't picked one yet — saves a click for typical
one-profile/one-folder home setups.
- web/src/routes/admin/requests/+page.svelte: friendly toast copy for
the new error codes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Decode errors in LookupArtist/AlbumByMBID now wrap ErrInvalidPayload so
callers can errors.Is the same way they do against the M5a methods.
- Tighten TestLookupAlbumByMBID_BadJSON to assert on ErrInvalidPayload.
- Add empty-mbid rejection tests for both methods.
- Add TestLookupArtistByMBID_EmptyArrayReturnsErrNotFound (only the album
variant had the test before — symmetric coverage now).
- Normalize the network-error test's api key to key123 for consistency
with the rest of delete_test.go.
- Add ArtistMBID/AlbumMBID to LookupResult so handlers can build the
full lidarr_requests row from album/track lookups (parent MBID is
needed for the AddAlbum API call and for the schema's NOT NULL
lidarr_artist_mbid)
- Trim trailing slash on BaseURL before joining paths to avoid
double-slash URLs when operators enter "http://lidarr.lan/"
- Check json.Marshal errors in AddArtist/AddAlbum
- Extract lidarrImage as a named unexported type instead of repeating
the anonymous struct three times
- Add NewClient(baseURL, apiKey) constructor with 30s default timeout
- Add ErrLookupFailed test coverage for the 4xx-non-auth branch on
both get and post helpers
- Rename TestListRootFolders_BadJSON -> _NullBody (it tests null,
not malformed); add a real malformed-JSON test
- Defensive separator guard on LookupAlbum.Secondary so an empty
ArtistName doesn't produce a leading " · "
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>