Closes#480. Routes the external-JSON playout remote path through the same IRemoteStreamProber seam #473/PR #479 added for generated playout, so a media item gone from the server renders a real error card instead of ffmpeg exit 8 looping on the same dead item.
Change (ExternalJsonPlayoutItemProvider.StreamRemotely): probe the /media/plex/... URL before returning it (and before the Plex metadata round-trip). Unavailable → PlayoutItemNotAvailableFromMediaServer (handler already maps this to a real-error card). Fail-open policy (redirected-404 only) lives inside the prober; per-provider probing keeps blast radius to one method (choke-point option rejected — the provider builds a synthetic MediaItem that doesn't fit ValidatePlayoutItemPath).
Tests: ExternalJsonPlayoutItemProviderTests pins both directions; non-vacuous (neutralizing the probe flips the unavailable case to Right). Fail-open contract stays pinned by HttpRemoteStreamProberTests.
Docs: docs/decisions.md#480 entry closes the #473 scope gap (append-only → cross-referenced, not edited in place).
Note: external-JSON channels have no DB PlayoutItem rows, so the error card uses the existing 1-min work-ahead clamp (repeated short cards within a dead slot) rather than spanning to the next item — strictly better than exit-8; documented in decisions.md.
Closes #480. Routes the external-JSON playout remote path through the same `IRemoteStreamProber` seam #473/PR #479 added for generated playout, so a media item gone from the server renders a real error card instead of ffmpeg exit 8 looping on the same dead item.
**Change** (`ExternalJsonPlayoutItemProvider.StreamRemotely`): probe the `/media/plex/...` URL before returning it (and before the Plex metadata round-trip). Unavailable → `PlayoutItemNotAvailableFromMediaServer` (handler already maps this to a real-error card). Fail-open policy (redirected-404 only) lives inside the prober; per-provider probing keeps blast radius to one method (choke-point option rejected — the provider builds a synthetic MediaItem that doesn't fit ValidatePlayoutItemPath).
**Tests**: `ExternalJsonPlayoutItemProviderTests` pins both directions; non-vacuous (neutralizing the probe flips the unavailable case to Right). Fail-open contract stays pinned by `HttpRemoteStreamProberTests`.
**Docs**: `docs/decisions.md` #480 entry closes the #473 scope gap (append-only → cross-referenced, not edited in place).
**Note**: external-JSON channels have no DB PlayoutItem rows, so the error card uses the existing 1-min work-ahead clamp (repeated short cards within a dead slot) rather than spanning to the next item — strictly better than exit-8; documented in decisions.md.
External-JSON playout channels build their own /media/plex/{server}/{plexFile}
URL in ExternalJsonPlayoutItemProvider.StreamRemotely and the handler assigns it
without routing through ValidatePlayoutItemPath, so the #473 class survived here:
a media item gone from the server 404s under ffmpeg (exit 8) and the same dead
item is re-selected for its whole slot.
Route StreamRemotely through the same IRemoteStreamProber seam #473/PR #479 added
for the generated-playout path. Probe runs before the Plex metadata round-trip
(the URL needs only server.Id + plexFile), so a gone item skips it. An unavailable
stream returns PlayoutItemNotAvailableFromMediaServer, which the handler already
maps to a real-error card. The fail-open policy (redirected-404 only) lives inside
IRemoteStreamProber, so this second call site duplicates only the decision to probe.
Tests: ExternalJsonPlayoutItemProviderTests pins both directions; proven non-vacuous
by neutralizing the probe. docs/decisions.md gets a #480 entry closing the #473
scope gap (append-only: the old #473 entry is cross-referenced, not edited).
fixes#480
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Independently verified: probe placement (URL depends only on server.Id+plexFile, so probe-first loses nothing and skips the metadata round-trip), control-flow (returning PlayoutItemNotAvailableFromMediaServer correctly bypasses the is UnableToLocatePlayoutItem filler gate at handler:228 and lands in the error-card case at handler:555 — consistent with the generated path), cancellation contract (propagates, matches ValidatePlayoutItemPath), fail-open (call site adds no independent close path; policy stays in HttpRemoteStreamProber), DI (Startup.cs:1086-1087), and the 1-minute-clamp claim (external-JSON has no DB PlayoutItems → maybeNextStart None → 1-min card, not a tight loop). Tests reach StreamRemotely and are non-vacuous.
Two cosmetic nits only, both non-blocking: test seeds PlayoutSource=Generated on an external-JSON channel (harmless — routing keys off ScheduleKind); dead-air filler skip is the documented, intended behavior. Not fixing them to avoid a second uncancellable CI run.
Independent cold-context adversarial review complete (review-only agent, fresh context).
Independently verified: probe placement (URL depends only on `server.Id`+`plexFile`, so probe-first loses nothing and skips the metadata round-trip), control-flow (returning `PlayoutItemNotAvailableFromMediaServer` correctly bypasses the `is UnableToLocatePlayoutItem` filler gate at handler:228 and lands in the error-card `case` at handler:555 — consistent with the generated path), cancellation contract (propagates, matches `ValidatePlayoutItemPath`), fail-open (call site adds no independent close path; policy stays in `HttpRemoteStreamProber`), DI (`Startup.cs:1086-1087`), and the 1-minute-clamp claim (external-JSON has no DB PlayoutItems → `maybeNextStart` None → 1-min card, not a tight loop). Tests reach `StreamRemotely` and are non-vacuous.
Two cosmetic nits only, both non-blocking: test seeds `PlayoutSource=Generated` on an external-JSON channel (harmless — routing keys off `ScheduleKind`); dead-air filler skip is the documented, intended behavior. Not fixing them to avoid a second uncancellable CI run.
Review-verdict: MERGEABLE @ 4a9502cbb5b2a82384e06a5cff73978afd4f2acf
timothy
merged commit 3c5f4e867a into main2026-07-20 00:37:29 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #480. Routes the external-JSON playout remote path through the same
IRemoteStreamProberseam #473/PR #479 added for generated playout, so a media item gone from the server renders a real error card instead of ffmpeg exit 8 looping on the same dead item.Change (
ExternalJsonPlayoutItemProvider.StreamRemotely): probe the/media/plex/...URL before returning it (and before the Plex metadata round-trip). Unavailable →PlayoutItemNotAvailableFromMediaServer(handler already maps this to a real-error card). Fail-open policy (redirected-404 only) lives inside the prober; per-provider probing keeps blast radius to one method (choke-point option rejected — the provider builds a synthetic MediaItem that doesn't fit ValidatePlayoutItemPath).Tests:
ExternalJsonPlayoutItemProviderTestspins both directions; non-vacuous (neutralizing the probe flips the unavailable case to Right). Fail-open contract stays pinned byHttpRemoteStreamProberTests.Docs:
docs/decisions.md#480 entry closes the #473 scope gap (append-only → cross-referenced, not edited in place).Note: external-JSON channels have no DB PlayoutItem rows, so the error card uses the existing 1-min work-ahead clamp (repeated short cards within a dead slot) rather than spanning to the next item — strictly better than exit-8; documented in decisions.md.
External-JSON playout channels build their own /media/plex/{server}/{plexFile} URL in ExternalJsonPlayoutItemProvider.StreamRemotely and the handler assigns it without routing through ValidatePlayoutItemPath, so the #473 class survived here: a media item gone from the server 404s under ffmpeg (exit 8) and the same dead item is re-selected for its whole slot. Route StreamRemotely through the same IRemoteStreamProber seam #473/PR #479 added for the generated-playout path. Probe runs before the Plex metadata round-trip (the URL needs only server.Id + plexFile), so a gone item skips it. An unavailable stream returns PlayoutItemNotAvailableFromMediaServer, which the handler already maps to a real-error card. The fail-open policy (redirected-404 only) lives inside IRemoteStreamProber, so this second call site duplicates only the decision to probe. Tests: ExternalJsonPlayoutItemProviderTests pins both directions; proven non-vacuous by neutralizing the probe. docs/decisions.md gets a #480 entry closing the #473 scope gap (append-only: the old #473 entry is cross-referenced, not edited). fixes #480 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Independent cold-context adversarial review complete (review-only agent, fresh context).
Independently verified: probe placement (URL depends only on
server.Id+plexFile, so probe-first loses nothing and skips the metadata round-trip), control-flow (returningPlayoutItemNotAvailableFromMediaServercorrectly bypasses theis UnableToLocatePlayoutItemfiller gate at handler:228 and lands in the error-cardcaseat handler:555 — consistent with the generated path), cancellation contract (propagates, matchesValidatePlayoutItemPath), fail-open (call site adds no independent close path; policy stays inHttpRemoteStreamProber), DI (Startup.cs:1086-1087), and the 1-minute-clamp claim (external-JSON has no DB PlayoutItems →maybeNextStartNone → 1-min card, not a tight loop). Tests reachStreamRemotelyand are non-vacuous.Two cosmetic nits only, both non-blocking: test seeds
PlayoutSource=Generatedon an external-JSON channel (harmless — routing keys offScheduleKind); dead-air filler skip is the documented, intended behavior. Not fixing them to avoid a second uncancellable CI run.Review-verdict: MERGEABLE @
4a9502cbb5