- A new request still pending after any auto-approval notifies the
admins (request_pending), but not the requester if they are an admin.
A request that dedups into one already in flight is not announced again.
- Approving or rejecting a request notifies the requester, and a
rejection carries the admin's notes as the reason. An admin deciding
their own request gets nothing.
- The reconciler notifies the requester when their request arrives
(request_completed), linking the matched album or artist.
- A request the re-acquisition sweeper files and cannot approve itself
notifies the admins.
- A quarantine flag notifies every admin except the flagger, naming the
track, the flagger and the reason.
lidarrrequests.Service.CreateTracked reports whether a request was
inserted or deduped; Create wraps it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-acquisition targets albums with ANY track missing, and completion only
asked for a track on disk, so the tracks that never left completed every
re-acquisition request the moment Lidarr accepted the add (52 on the deploy,
each ~150ms after its add).
An album or track request now completes when an album named by its release
or group has a track on disk AND either a track arrived after the request
(a new album, or Lidarr fetching another release into its own row) or no
track that was missing at the request is still missing. added_at is the
arrival clock; updated_at moves on every tag re-read.
Migration 0071 reopens completed album/track requests whose matched album
fails that test, as approved with the match cleared; the Lidarr add stays
confirmed, so nothing is re-sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lidarr's metadata is keyed by MusicBrainz release group, but re-acquisition
requested albums by their release id, so every add came back "not found".
- Sweeper requests an album by its tag-supplied release group, else the one
MusicBrainz names (cached onto the album). An album MusicBrainz cannot name
is skipped and counted, with no attempt spent.
- Reconciler: an add refused as not found re-reads the request's album id as
a release (library first, then MusicBrainz), rewrites the request to the
group and adds again. This repairs the requests already stored.
- Completion matches an album by release id or release group, and only once a
track of it is on disk, so a re-acquisition request no longer completes
against the row of the album it is trying to bring back.
Closes#5241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The hand-unrolled byte-pair version in reconciler.go tripped gofmt -s
and goimports. Replaced with the same loop-based formatter that
internal/library/eventbus.go uses — same behaviour, fewer lines, lint
clean. Two copies of the helper still exist (one per package) to keep
the no-back-edge property for both internal/library and
internal/lidarrrequests, but they're now identical and the duplication
is tiny.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 3b — extends event publishing to background workers.
When the reconciler matches an approved Lidarr request against the
library and flips it to 'completed', it now publishes a
request.status_changed event scoped to the original requester so their
/requests page invalidates without polling. Three call sites — one per
kind (artist / album / track) — capture the completed row instead of
discarding it so the publish has the user_id.
Bus plumbing: the reconciler runs in cmd/minstrel/main.go which
constructs services before server.New is called. Moved bus
construction into main; the Server struct gained a Bus field that
Router() reads, falling back to a fresh local instance when nil
(test contexts). Result: one bus per process shared by every
publisher and the SSE subscriber endpoint.
reconciler.go inlines a uuid->string helper rather than reaching into
internal/api for one — avoids a back-edge dependency.
Test compat: 9 NewReconciler call sites in reconciler_integration_test.go
get `nil` for the new bus param. The reconciler's publishCompleted helper
is a no-op when the bus is nil so tests that don't care about events
keep passing.
Scanner producer (scan.progress) deferred to slice 3c — needs more
thought about which lifecycle transitions warrant events vs. flood the
stream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>