feat(api): channel on-air / now-playing state endpoint (#97) #112

Merged
timothy merged 7 commits from issue-97-channel-state-api into docs/59-ui-redesign-brief 2026-07-04 07:48:47 +02:00
Owner

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

## 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)
timothy added 7 commits 2026-07-02 21:53:51 +02:00
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>
fix(api): surface programs not filler in channel state, tighten queries
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
73455aae28
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>
timothy merged commit 68f073ed42 into docs/59-ui-redesign-brief 2026-07-04 07:48:47 +02:00
Sign in to join this conversation.