fix(480): probe external-JSON remote-stream URLs before ffmpeg #486

Merged
timothy merged 1 commits from fix/480-external-json-probe into main 2026-07-20 00:37:29 +02:00
Owner

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.
timothy added 1 commit 2026-07-20 00:17:32 +02:00
fix(480): probe external-JSON remote-stream URLs before handing them to ffmpeg
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m26s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m54s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
4a9502cbb5
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>
Author
Owner

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 @ 4a9502cbb5

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 main 2026-07-20 00:37:29 +02:00
timothy deleted branch fix/480-external-json-probe 2026-07-20 00:37:30 +02:00
Sign in to join this conversation.