fix(68): rebuild on-demand channel guide (and mirrors) on thaw #540

Merged
timothy merged 1 commits from fix/68-ondemand-guide-refresh into main 2026-07-21 19:49:15 +02:00
Owner

Fixes #68 — on-demand resume/bookmark: keep the cached XMLTV guide in sync with the resumed playout timeline.

What

On-demand channels (PlayoutMode.OnDemand) already resume where the viewer stopped (Playout.OnDemandCheckpoint + PlayoutTimeShifter.TimeShift), and freeze guide+playback together (both read PlayoutItem.Start/Finish; the shift rewrites GuideStart/GuideFinish too) — so the free-running-clock desync #68 was filed about is structurally avoided. The gap: TimeShift rewrote the rows but the tune-in path never rebuilt the cached XMLTV fragment, so an external EPG client could see a stale timeline after a thaw.

Fix

IPlayoutTimeShifter.TimeShift returns the channel numbers whose cached guide is now stale — the shifted channel plus its mirrors (same fan-out BuildPlayoutHandler already does) — and TimeShiftOnDemandPlayoutHandler enqueues RefreshChannelData for each on CancellationToken.None.

Verification

  • 1793 tests green; 9 new (handler enqueue + mirror + no-shift; shifter source+mirrors / Continuous / zero-offset / active-unforced / never-watched). Non-vacuity proven by a compiling negative control.
  • Two cold adversarial reviews (initial + delta re-review of the mirror fan-out) → MERGEABLE.
  • Live-E2E not required: this is an internal background MediatR command, not an /api/* write-path handler.

Docs

channels.md (On-demand resume), domain-model.md, decisions.md (scheduling.ondemand-guide-refresh-on-thaw).

Per-viewer resume intentionally out of scope (single per-channel checkpoint; #68 says per-channel suffices).

Fixes #68 — on-demand resume/bookmark: keep the cached XMLTV guide in sync with the resumed playout timeline. ## What On-demand channels (`PlayoutMode.OnDemand`) already resume where the viewer stopped (`Playout.OnDemandCheckpoint` + `PlayoutTimeShifter.TimeShift`), and freeze guide+playback together (both read `PlayoutItem.Start/Finish`; the shift rewrites `GuideStart/GuideFinish` too) — so the free-running-clock desync #68 was filed about is structurally avoided. The gap: `TimeShift` rewrote the rows but the tune-in path never rebuilt the **cached** XMLTV fragment, so an external EPG client could see a stale timeline after a thaw. ## Fix `IPlayoutTimeShifter.TimeShift` returns the channel numbers whose cached guide is now stale — the shifted channel **plus its mirrors** (same fan-out `BuildPlayoutHandler` already does) — and `TimeShiftOnDemandPlayoutHandler` enqueues `RefreshChannelData` for each on `CancellationToken.None`. ## Verification - 1793 tests green; 9 new (handler enqueue + mirror + no-shift; shifter source+mirrors / Continuous / zero-offset / active-unforced / never-watched). Non-vacuity proven by a compiling negative control. - Two cold adversarial reviews (initial + delta re-review of the mirror fan-out) → MERGEABLE. - Live-E2E not required: this is an internal background MediatR command, not an `/api/*` write-path handler. ## Docs channels.md (On-demand resume), domain-model.md, decisions.md (`scheduling.ondemand-guide-refresh-on-thaw`). Per-viewer resume intentionally out of scope (single per-channel checkpoint; #68 says per-channel suffices).
timothy added 1 commit 2026-07-21 19:13:08 +02:00
fix(68): rebuild on-demand channel guide (and mirrors) on thaw
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 27s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m19s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m26s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m52s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
dfed9a393b
An on-demand channel (`PlayoutMode.OnDemand`) already is the "resume where I
left off" feature: `Playout.OnDemandCheckpoint` persists the viewer's position
and `PlayoutTimeShifter.TimeShift` slides the materialized timeline forward on
tune-in so the paused item is active again. Because it rewrites `GuideStart`/
`GuideFinish` alongside `Start`/`Finish`, guide and playback freeze together —
structurally avoiding the free-running-wall-clock desync #68 was filed about.

The one gap: `TimeShift` rewrote the stored `PlayoutItem` rows but the XMLTV
guide is served from a cached fragment that only `RefreshChannelData` rebuilds,
and the tune-in path never enqueued it. So an external EPG client polling after
a thaw could see a stale timeline until the next incidental rebuild.

Fix: `IPlayoutTimeShifter.TimeShift` now returns the channel numbers whose cached
guide is stale — the shifted channel plus any channels that mirror it (the same
fan-out `BuildPlayoutHandler` already does) — and `TimeShiftOnDemandPlayoutHandler`
enqueues a `RefreshChannelData` for each on `CancellationToken.None` (post-commit
side effect must not be abandoned if the session token cancels).

Tests: handler enqueues a rebuild per stale channel (+ mirror + no-shift cases);
`PlayoutTimeShifter` reports source+mirrors on a shift, empty on Continuous /
zero-offset / active-unforced, and correctly seeds+shifts a never-watched playout.
Non-vacuity of the enqueue proven by a compiling negative control.

Docs: channels.md (On-demand resume section), domain-model.md, decisions.md
(scheduling.ondemand-guide-refresh-on-thaw). Per-viewer resume is out of scope
(single per-channel checkpoint; #68 says per-channel suffices).

fixes #68

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Review-verdict: MERGEABLE @ dfed9a393b

Two cold, review-only adversarial passes over this diff (a different model-context each time):

  • Initial review of the core fix → MERGEABLE, one Medium found: mirror-channel guides not refreshed on thaw.
  • That Medium was fixed (source + mirror fan-out, reusing the BuildPlayoutHandler pattern) and the enqueue moved to CancellationToken.None (post-commit). A delta re-review of the mirror fan-out + Option<string>List<string> refactor confirmed no regressions (NSubstitute null-list trap checked at both BuildPlayoutHandler call sites; negative controls non-vacuous) → MERGEABLE.

1793 tests green (9 new).

Review-verdict: MERGEABLE @ dfed9a393b445c0068056cf073a25aee350ab01b Two cold, review-only adversarial passes over this diff (a different model-context each time): - **Initial** review of the core fix → MERGEABLE, one Medium found: mirror-channel guides not refreshed on thaw. - That Medium was fixed (source **+ mirror** fan-out, reusing the `BuildPlayoutHandler` pattern) and the enqueue moved to `CancellationToken.None` (post-commit). A **delta re-review** of the mirror fan-out + `Option<string>`→`List<string>` refactor confirmed no regressions (NSubstitute null-list trap checked at both `BuildPlayoutHandler` call sites; negative controls non-vacuous) → MERGEABLE. 1793 tests green (9 new).
Author
Owner

CI red is an unrelated flaky SPA test — needs an operator job re-run

Build & test (.NET) (run 960 / job 5342) failed on web/src/screens/LibrariesScreen.test.tsx:292"stops scan polling and refreshes sources once when scans complete" — a waitFor that timed out expecting the 75% progress bar to be removed. This is the known CI-VM waitFor-timeout flake class (see MEMORY CI VM test timeouts).

It is not this PR:

  • This diff touches zero web/ files (C# + docs only) — git diff --name-only origin/main...HEAD -- web/ is empty.
  • LibrariesScreen.test.tsx passes 3/3 locally (22/22 tests each run).
  • Full C# suite: 1793 green.

Agent-side re-run/cancel is not possible on this Gitea (1.25.4). Please re-run the failed Build & test (.NET) job from the Gitea UI; it should pass. Once green, the H6/H10 gate auto-grants the merge (all Done-when boxes ticked, Review-verdict: MERGEABLE @ dfed9a39 posted).

## CI red is an unrelated flaky SPA test — needs an operator job re-run `Build & test (.NET)` (run 960 / job 5342) failed on **`web/src/screens/LibrariesScreen.test.tsx:292`** — *"stops scan polling and refreshes sources once when scans complete"* — a `waitFor` that timed out expecting the 75% progress bar to be removed. This is the known CI-VM `waitFor`-timeout flake class (see MEMORY `CI VM test timeouts`). **It is not this PR:** - This diff touches **zero `web/` files** (C# + docs only) — `git diff --name-only origin/main...HEAD -- web/` is empty. - `LibrariesScreen.test.tsx` passes **3/3 locally** (22/22 tests each run). - Full C# suite: **1793 green**. Agent-side re-run/cancel is not possible on this Gitea (1.25.4). **Please re-run the failed `Build & test (.NET)` job from the Gitea UI**; it should pass. Once green, the H6/H10 gate auto-grants the merge (all Done-when boxes ticked, `Review-verdict: MERGEABLE @ dfed9a39` posted).
timothy merged commit d9eb307d71 into main 2026-07-21 19:49:15 +02:00
timothy deleted branch fix/68-ondemand-guide-refresh 2026-07-21 19:49:15 +02:00
Sign in to join this conversation.