The new POST /api/media-sources/{plex|jellyfin|emby}/{id}/scan-collections
endpoints acquire a per-provider collections lock (409 if held) and hand the
single release to the ScannerService finally. But SchedulerService's periodic
collection scans were enqueued WITHOUT the lock, and ScannerService's finally
released the collections lock whenever held with no ownership check. A
scheduler-queued scan running while an API request held the lock cross-released
the API's lock (#250 bug class), letting a second API request get a spurious
202 instead of 409.
Fix (mirrors the SynchronizePlexLibraryByIdIfNeeded(Unlock: !networksFollow)
library-scan precedent):
- Add `bool Unlock = true` (4th positional param) to the three
Synchronize{Plex,Jellyfin,Emby}Collections records; default keeps the
controller + Libraries.razor call sites compiling and releasing on run.
- ScannerService: the three collection finallys now honor `request.Unlock`
(the concrete typed request is in scope in each method) so a batch member
with Unlock:false never releases a lock it doesn't own.
- SchedulerService: replace the unlocked per-source enqueue with a lock-once
per-provider batch — LockX Collections() once, enqueue each source with
Unlock:isLast (last message owns the release), compensating unlock in catch,
and SKIP the whole provider loop if the lock is already held. A naive
"lock-per-source, skip if held" would deterministically starve the 2nd+
source; lock-once-batch does not.
Tests (ErsatzTV.Tests/Services/): ScannerServiceCollectionLockTests drives the
real ScannerService read loop + real EntityLocker and asserts Unlock:false
leaves a held lock intact while Unlock:true releases (all three providers);
SchedulerServiceCollectionLockTests reflect-invokes ScanPlexMediaSources and
asserts it locks once + skips the enqueue when held, and hands the release to
the last message when acquired. Proven non-vacuous: reverting the Plex fix
fails exactly the three Plex tests.
No OpenAPI/v1.json change (internal channel-message record, not a DTO).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review (fork + Codex, both flagged) of PR #241:
- SPA (both reviewers): removing PENDING_GRACE_TICKS wholesale reintroduced a
stuck scan button. A 202'd scan that finishes between 10s polls (short/empty
library) is never observed active, so its optimistic pending flag wedged the
button disabled until reload. Restore a BOUNDED grace net (pruneGraceExpiredPending)
— re-scoped honestly: it absorbs the inherent queue->observed-active lag and the
fast-completion race, NOT the removed lying-200 compensation (the POST now returns
409/404/422 honestly). Bounds pending to PENDING_GRACE_TICKS * pollMs (~30s).
- SPA 409 (Codex): on "already scanning" the button was cleared+reconciled, but a
scan-status still lagging the in-progress scan re-enabled the button and let the
user fire repeated 409s. Keep the pending flag on 409 (no toast) so the button
stays disabled; polling promotes or expires it.
- Scheduler (Codex): the Plex-Shows tail-token batch and the local/Jellyfin/Emby
scan enqueues had no compensating unlock — a WriteAsync failure after LockLibrary
(cancellation on shutdown) stranded the library lock. Wrap each acquired-lock
enqueue in try/catch → UnlockLibrary → rethrow (the Plex catch covers both writes,
since the library message carries Unlock: false and the un-enqueued networks
message was the sole releaser).
Tests: two new App.test.tsx cases — grace-window expiry re-enables the button, and
409 keeps it disabled through the queue->active lag.
Ref #232.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scan queue handler now returns a QueueLibraryScanResult enum
(Queued|NotFound|SyncDisabled|AlreadyScanning) instead of a lying bool;
LibrariesController.ScanLibrary maps them to 202/404/422/409 with ProblemDetails.
Guard the lock->enqueue with the EnqueueWithTraktLock compensating-unlock pattern.
ScannerService now releases every library/collection lock in a finally so a handler
exception can't leak the lock. Plex "Shows" scheduler batch (one lock, two messages)
now has only the trailing SynchronizePlexNetworks carry the single release
(Unlock flag), mirroring the scheduler Trakt tail-token precedent.
Guard the other lock->enqueue producers (Create/UpdateLocalLibrary, UpdateTraktList)
with compensating unlock. SPA drops the PENDING_GRACE_TICKS heuristic now that the
POST reports 202/409/404/422 directly: 202 -> pending+poll, 409 -> reconcile (no
error toast), 404/422 -> surface error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix validation in new form layout
* pin mediatr to last oss version
* update dependencies
* cleanup code in core
* cleanup code in ffmpeg
* cleanup code in infra
* cleanup code in scanner
* cleanup code in application
* cleanup main code
* cleanup test code
* solution-wide code cleanup
* initial plumbing
* scan for plex networks
* save network contents to db as tags
* eliminate network tag id churn
* add network and show_network to search index
* update last networks scan
* show networks on tv show page
* update changelog
* fix embedded subtitles from media servers
* fix plex external subtitles
* fix artwork bug, delete orphaned subtitles
* jellyfin subtitles work again
* emby subtitles work
* rescan all media server libraries
* initial work on maintaining playout state
* debugging wip
* fix refresh playout logic
* fix failing test
* more fixes
* update changelog
* comment out some debug logs
* comment out more logs