---
key: media.remote-stream-probe-externaljson
title: '2026-07-20 — External-JSON playout channels now probe the remote-stream URL too, closing the #473 scope gap (#480)'
status: active
since: '2026-07-20'
supersedes: none
superseded-by: none
rule: 'External-JSON playout channels'' `StreamRemotely` now probes the remote-stream URL through the same `IRemoteStreamProber` seam as the generated-playout path, closing the #473 scope gap for a channel kind with no DB `PlayoutItem` rows.'
signals: 'external-JSON channels, remote-stream probe, scope-gap follow-up · paths: `ExternalJsonPlayoutItemProvider.StreamRemotely` · issues: #480, #473'
mechanics: '`ExternalJsonPlayoutItemProviderTests`'
---

The #473 fix (PR #479, the entry above dated 2026-07-19) probed Plex/Jellyfin/Emby remote-stream URLs in
`GetPlayoutItemProcessByChannelNumberHandler.ValidatePlayoutItemPath`, but explicitly scoped itself to the
**generated-playout** path and flagged `ExternalJsonPlayoutItemProvider` as the surviving hole (tracked as
#480). That provider builds its own `/media/plex/{server}/{plexFile}` URL in `StreamRemotely` and its result
is assigned in the handler *without* passing through `ValidatePlayoutItemPath`, so for external-JSON channels
a media item gone from the server still 404'd under ffmpeg (exit 8) and the same dead item was re-selected
for its whole slot. This closes that gap.

- **Fix location: the provider, not a new handler choke point.** `StreamRemotely` now probes the URL via the
  same `IRemoteStreamProber` seam before returning it, mirroring the three generated-playout branches. The
  handler-side single-choke-point option (route *every* `PlayoutItemWithPath` through one validator) was
  rejected: the external-JSON provider constructs a **synthetic** `MediaItem`/`PlayoutItem` (from the JSON +
  Plex API) whose path is already a `/media/plex/...` URL, so it does not fit `ValidatePlayoutItemPath`'s
  local-path-then-per-provider-switch derivation without contortion. Per-provider probing keeps the blast
  radius to one method; the fail-open **policy** (only a *redirected* 404 fails closed) lives entirely inside
  `IRemoteStreamProber`, so the second call site duplicates only the *decision to probe*, not the policy.
- **Probe before the Plex metadata round-trip.** The URL depends only on `server.Id` + `program.PlexFile`,
  not on the constructed `MediaItem`, so the probe runs *before* `GetPlexEpisode`/`GetPlexMovie`. A gone item
  therefore also skips the Plex API metadata call (which would otherwise `throw NotSupportedException` on a
  `Left`).
- **Error type + card sizing.** An unavailable stream returns `PlayoutItemNotAvailableFromMediaServer`, which
  the handler already maps (case added in #473) to a real-error card rather than "Channel is Offline". One
  behavioural difference from the generated path is accepted: external-JSON channels have **no DB
  `PlayoutItem` rows** (their schedule is a JSON file), so the handler's `maybeNextStart` query returns none
  and the error card falls to the existing 1-minute work-ahead clamp instead of spanning to the next item.
  The worker then re-tunes and advances by time. That is repeated 1-minute error cards within a long dead
  slot rather than one long card — strictly better than the exit-8 loop, and sizing to the next JSON program
  would require parsing the schedule here (deferred, out of scope).
- **Tests.** `ExternalJsonPlayoutItemProviderTests` pins both directions (unavailable → `Left`
  `PlayoutItemNotAvailableFromMediaServer` and no metadata call; available → `Right` with the `/media/plex`
  URL). The fail-open contract itself stays pinned by `HttpRemoteStreamProberTests`. Proven non-vacuous by
  neutralizing the probe guard and watching the unavailable assertion flip to `Right`.
