Adds a channel runtime-state endpoint for the ChicoryTV SPA: per-channel on-air status (HLS segmenter activity) plus the currently playing program with UTC start/finish bounds.
nowPlaying resolves like the XMLTV guide: pre/mid-roll filler surfaces the guide group's program (title + full program bounds), never the filler; a guide entry with only filler (e.g. fallback loop) reports null.
Mirror channels resolve via MirrorSourceChannelId; PlayoutOffset is subtracted for lookup and re-added to the returned bounds (matches streaming + XMLTV paths).
Two bounded queries total (no N+1): a cheap covering-item projection per distinct playout offset, then one metadata query scoped to the covering guide groups.
Now travels on the MediatR request, so boundary semantics (now == start playing, now == finish not) are pinned by tests.
Caveats
#99: on-air detection is HLS-segmenter-only; MPEG-TS / HLS-Direct sessions report onAir: false while streaming (documented in the spec, tracked in #99).
OnDemand channels report state from stored playout items, which only re-anchor on tune-in (documented in the spec).
Review
Adversarially reviewed (high-effort agent pass over the full diff): 0 blockers, 2 majors (filler surfacing, query-window perf cliff) and all minors/nits fixed in 73455aae — including a regression the review's mirror-test nit exposed (source channel inheriting its offset mirror's lookup result). OpenAPI + generated web types regenerated; check:api green; 295 ErsatzTV.Tests + 488 Core.Tests pass.
## Summary
Adds a channel runtime-state endpoint for the ChicoryTV SPA: per-channel on-air status (HLS segmenter activity) plus the currently playing program with UTC start/finish bounds.
## Endpoint
- `GET /api/channels/state` → `ChannelStateResponseModel[]` — `{ channelId, channelNumber, onAir, nowPlaying: { title, startUtc, finishUtc } | null }`
Behavior:
- `nowPlaying` resolves like the XMLTV guide: pre/mid-roll filler surfaces the guide group's **program** (title + full program bounds), never the filler; a guide entry with only filler (e.g. fallback loop) reports `null`.
- Mirror channels resolve via `MirrorSourceChannelId`; `PlayoutOffset` is subtracted for lookup and re-added to the returned bounds (matches streaming + XMLTV paths).
- Two bounded queries total (no N+1): a cheap covering-item projection per distinct playout offset, then one metadata query scoped to the covering guide groups.
- `Now` travels on the MediatR request, so boundary semantics (`now == start` playing, `now == finish` not) are pinned by tests.
## Caveats
- **#99**: on-air detection is HLS-segmenter-only; MPEG-TS / HLS-Direct sessions report `onAir: false` while streaming (documented in the spec, tracked in #99).
- OnDemand channels report state from stored playout items, which only re-anchor on tune-in (documented in the spec).
## Review
Adversarially reviewed (high-effort agent pass over the full diff): 0 blockers, 2 majors (filler surfacing, query-window perf cliff) and all minors/nits fixed in `73455aae` — including a regression the review's mirror-test nit exposed (source channel inheriting its offset mirror's lookup result). OpenAPI + generated web types regenerated; `check:api` green; 295 ErsatzTV.Tests + 488 Core.Tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
An off-air channel has no current playout item, so the handler returns
null for NowPlaying; the DTO now declares that (file-scoped #nullable —
ErsatzTV.Core has NRT disabled project-wide). Regenerated OpenAPI and
web types: nowPlaying keeps its nullable oneOf schema (matching the
schema contract test) and channelNumber loses a spurious nullable
annotation left over from an earlier draft — the field is required.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review fixes for the #97 endpoint:
- nowPlaying now resolves like the XMLTV guide: when the wall-clock item
is pre/mid-roll filler, surface the guide group's program item (title +
full program bounds) instead of the filler; a guide entry with no
program item (e.g. fallback loop) reports null. Previously an ad break
would retitle the channel and reset progress every 30 seconds.
- covering-item lookup now runs one cheap projected query per distinct
playout offset instead of hydrating the full metadata include tree for
a window widened by the largest offset across ALL channels (up to 24h);
metadata is fetched in a second query scoped to the covering guide
groups, time-bounded to stay safe against GuideGroup recycling (mod
10000). Regression caught while testing: covering items are keyed by
(source channel, lookup time) so a source channel no longer inherits
its offset mirror's lookup result.
- GetChannelStatesForApi now carries Now explicitly (controller passes
DateTime.UtcNow), making boundary semantics testable: new tests pin
now == Start (playing) and now == Finish (not playing), plus mid-roll,
filler-only, and mirror-source-row cases.
- ChannelNowPlayingResponseModel title is non-nullable in the schema
(#nullable enable) — GetDisplayTitle always returns a string.
- GetState uses the typed-return controller convention; Playouts.Mapper
reverted to internal (handler lives in the same assembly).
- Spec doc notes the OnDemand drift caveat and filler semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
Adds a channel runtime-state endpoint for the ChicoryTV SPA: per-channel on-air status (HLS segmenter activity) plus the currently playing program with UTC start/finish bounds.
Endpoint
GET /api/channels/state→ChannelStateResponseModel[]—{ channelId, channelNumber, onAir, nowPlaying: { title, startUtc, finishUtc } | null }Behavior:
nowPlayingresolves like the XMLTV guide: pre/mid-roll filler surfaces the guide group's program (title + full program bounds), never the filler; a guide entry with only filler (e.g. fallback loop) reportsnull.MirrorSourceChannelId;PlayoutOffsetis subtracted for lookup and re-added to the returned bounds (matches streaming + XMLTV paths).Nowtravels on the MediatR request, so boundary semantics (now == startplaying,now == finishnot) are pinned by tests.Caveats
onAir: falsewhile streaming (documented in the spec, tracked in #99).Review
Adversarially reviewed (high-effort agent pass over the full diff): 0 blockers, 2 majors (filler surfacing, query-window perf cliff) and all minors/nits fixed in
73455aae— including a regression the review's mirror-test nit exposed (source channel inheriting its offset mirror's lookup result). OpenAPI + generated web types regenerated;check:apigreen; 295 ErsatzTV.Tests + 488 Core.Tests pass.🤖 Generated with Claude Code