Backend: channel on-air / now-playing state API (for Channels screen live treatment) #97
Closed
opened 2026-07-02 18:48:48 +02:00 by timothy
·
9 comments
No Branch/Tag Specified
main
913-scripted-playout-401
renovate/meziantou.analyzer-3.x
renovate/nuget-sixlabors.imagesharp-vulnerability
renovate/dotnet-monorepo
release/v26.15.0-notes
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Blocks
Reference: timothy/ersatztv#97
Reference in New Issue
Block a user
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.
The Channels prototype (
design-system/templates/chicorytv-admin/Channels.jsx) has an On-air filter/segment, an accent-rail +live-softrow treatment for on-air channels (c.live), and a "Now playing" column (current program title + progress %). None of this is in the channel list API — it is runtime playout/session state, not channel config.Do
PlayoutItemtitle and start/finish (to derive progress %).GET /api/channels/stateor enrich from active sessions/playout) so the static channel list DTO stays cheap; the SPA can poll it.Acceptance: #84 can show the On-air count/filter, the live row treatment, and the Now-playing lane from real data — and degrades gracefully (Idle / Off air) when unavailable.
Supports #84 (live treatment). Related: #72 (channel-list health/status states), #60 (browser playback). Parent: #59.
Picking this up. I’m first checking the existing channel API and playout/session state paths so the implementation can keep
/api/channelsstatic and expose runtime state through a separate, pollable endpoint as requested.Design clarification accepted:
nowPlayingwill be the current scheduledPlayoutItemby wall-clock, independent of whether anyone is streaming.onAirwill initially mean "has an active HLS segmenter session" viaIFFmpegSegmenterService.IsActive(channelNumber).Important caveat: the current session registry only covers segmenter-backed modes (
HttpLiveStreamingSegmenterandTransportStreamHybrid). MPEG-TS direct and HLS-Direct can stream without registering inIFFmpegSegmenterService.Workers, so they will reportonAir: falseuntil direct-path session tracking exists.Opened follow-up #99 for that universal active-session tracking. #97 will stay scoped to the pollable batched state endpoint and current playout lookup.
Done
What was done: Task 1 contract/route skeleton added for
GET /api/channels/state: response records, MediatR query record, controller action, and controller test. Commit:7c1a1d56 feat(api): add channel state endpoint contract.Root cause: n/a
Files changed:
ErsatzTV.Core/Api/Channels/ChannelNowPlayingResponseModel.cs,ErsatzTV.Core/Api/Channels/ChannelStateResponseModel.cs,ErsatzTV.Application/Channels/Queries/GetChannelStatesForApi.cs,ErsatzTV/Controllers/Api/ChannelController.cs,ErsatzTV.Tests/Controllers/ChannelControllerTests.cs.Verification: RED observed first with missing
ChannelStateResponseModel,ChannelNowPlayingResponseModel,GetChannelStatesForApi, andChannelController.GetState; GREEN after implementation withdotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~ChannelControllerTests.GetState_Should_Return_200_With_Channel_State" -v minimalpassing 1/1.Deferred: Handler behavior, OpenAPI contract tests/schema work, and runtime state implementation remain for later tasks.
Follow-up issues: Existing follow-up #99 remains relevant for universal active-session tracking.
Docs updated: n/a for Task 1.
Code Quality Review - Task 1
Commit reviewed:
7c1a1d5672f6eb9eb5d1dbf77d767e03ead88918Code quality: APPROVED
Findings: No blocking code-quality findings.
Notes: The new
GetStateaction usesIActionResult/OkObjectResult, while nearby collection endpoints such asGetAllreturn typed lists directly. This is a minor consistency note, not a required fix for Task 1.Verification: Reviewed the commit diff and relevant controller/test/API model files. Ran
dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter FullyQualifiedName~ChannelControllerTests.GetState_Should_Return_200_With_Channel_State -v minimal; result: passed 1/1. Existing NU1903 package vulnerability warnings were reported during restore/build.Done
What was done: Task 2 handler implementation added for
GET /api/channels/state: batched current playout item lookup, active segmenter state viaIFFmpegSegmenterService.IsActive(channel.Number), and reuse of the existing playout display-title behavior. Commit:7b903d4bb41a3858fbae53c14a8e611cc6cdb302 feat(api): return channel runtime state.Root cause: n/a
Files changed:
ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs,ErsatzTV.Application/Playouts/Mapper.cs,ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs.Verification: RED observed first with missing
GetChannelStatesForApiHandler. GREEN after implementation withdotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests" -v minimalpassing 3/3. Existing NU1903 package vulnerability warnings were reported during restore/build.Deferred: OpenAPI/schema work and full feature/final verification remain for later tasks. Issue remains open.
Follow-up issues: Existing follow-up #99 remains relevant for universal active-session tracking beyond segmenter-backed modes.
Docs updated: n/a for Task 2.
Review
Scope: Task 3 spec compliance re-review after code-quality fix, commits
6057bceaandf01ffc62.Result: APPROVED for Task 3 spec compliance.
Findings: No remaining required fixes before code-quality review.
Verification:
TZ=UTC dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter FullyQualifiedName~OpenApiErrorResponseContractTests.Static_OpenApi_Should_Document_Channel_State_Response -v minimalpassed: 1 test passed, 0 failed. The run emitted existing NU1903 vulnerability warnings forMicrosoft.OpenApiandScriban.Signed.Notes: The static OpenAPI document now defines
GET /api/channels/statewith a 200 array ofChannelStateResponseModel; includesChannelStateResponseModelwithchannelId,channelNumber,onAir, and nullablenowPlaying; includesChannelNowPlayingResponseModelwithtitle,startUtc, andfinishUtc. The nullable$refshape uses the repo's existingoneOf: [{ type: "null" }, { $ref: ... }]style.Done
What was done: Fixed the final review finding for mirror channel state resolution.
GetChannelStatesForApiHandlernow resolvesnowPlayingagainstMirrorSourceChannelId ?? channel.Id, appliesPlayoutOffsetwhen looking up the source playout item, and returnsstartUtc/finishUtcadjusted back onto the channel wall-clock timeline. Commit:07e9d9838d66385339732a4693cd6de1ffbba359.Root cause: The handler looked up current playout items only by the API channel id and wall-clock
now, while playback resolves mirror channels through their source channel and subtracts the channel playout offset before locating the source item.Files changed:
ErsatzTV.Application/Channels/Queries/GetChannelStatesForApiHandler.cs,ErsatzTV.Tests/Application/Channels/GetChannelStatesForApiHandlerTests.cs.Verification: RED observed first with
dotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests.Handle_Should_Resolve_Mirror_Channel_NowPlaying_From_Source_With_Offset" -v minimal, failing becausemirrorState.NowPlayingwas null. GREEN after implementation with the same focused test passing 1/1, thendotnet test ErsatzTV.Tests/ErsatzTV.Tests.csproj --filter "FullyQualifiedName~GetChannelStatesForApiHandlerTests" -v minimalpassing 4/4. Existing NU1903 package vulnerability warnings were reported during restore/build.Deferred: Push/merge and issue closure left unchanged per the requested final-review-fix workflow.
Follow-up issues: Existing #99 remains relevant for universal active-session tracking beyond segmenter-backed modes.
Docs updated: n/a.
Final verification + review complete — PR #112 opened against
docs/59-ui-redesign-brief(stacked; this issue stays open until the stack reachesmain).Verification (after the interrupted runs): resolved the crash leftovers — the committed DTO was missing the nullable annotation the schema test expected, so
NowPlayingis nowChannelNowPlayingResponseModel?(e66f49e5; off-air ⇒null), and the dubious draftChannelNumber?was confirmed wrong and dropped. Full build, 295 ErsatzTV.Tests + 488 Core.Tests, OpenAPI regen,webgenerate:api+check:apiall green.Adversarial review (high-effort agent over the full #97 diff): verdict — no blockers; offset math, mirror resolution, boundary semantics, error contract, and honest #99 degradation all check out against the streaming/XMLTV precedents. Findings fixed in
73455aae:nowPlayingsurfaced raw filler (bumpers retitled the channel and reset progress every ad break) — now resolves the guide group's program like XMLTV; filler-only entries reportnull.Nowon the request (boundary tests now pinstart/finishsemantics), non-nullabletitlein the schema, typed controller return,Playouts.Mapperback tointernal, OnDemand-drift + filler semantics documented in the spec. The review's mirror-test nit exposed a real regression in my rewrite (source channel inheriting its mirror's lookup result) — fixed and pinned by a test.Scope caveat unchanged: on-air is HLS-segmenter-only; MPEG-TS/HLS-Direct tracking is #99.
Closed via PR #120 (stack merge to main, commit
1b6de047).What was done:
GET /api/channels/state(per-channelonAir+nowPlayingtitle/start/end) implemented in PR #112 (branchissue-97-channel-state-api), merged into the SPA stack, then landed on main with the #59 foundation via PR #120.Files: GetChannelStatesForApi(+Handler), ChannelStateResponseModel/ChannelNowPlayingResponseModel (Core/Api,
#nullable enablecompliant), ChannelController.GetState, handler + controller + OpenAPI contract tests; v1.json + SPA types regenerated.Deferred:
onAiris currently segmenter-session-based (HLS segmenter / TS hybrid only). Universal coverage for MPEG-TS direct + HLS-Direct is #99 — implementation in flight (PR #121, under review); the final wiring of the tracker into this endpoint happens as a small follow-up once #121 merges.Docs: docs/handoffs/chicorytv-issue-queue.md updated as part of the merge-pass session close-out.