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.
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).
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>
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).
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).
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.
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 readPlayoutItem.Start/Finish; the shift rewritesGuideStart/GuideFinishtoo) — so the free-running-clock desync #68 was filed about is structurally avoided. The gap:TimeShiftrewrote 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.TimeShiftreturns the channel numbers whose cached guide is now stale — the shifted channel plus its mirrors (same fan-outBuildPlayoutHandleralready does) — andTimeShiftOnDemandPlayoutHandlerenqueuesRefreshChannelDatafor each onCancellationToken.None.Verification
/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).
Review-verdict: MERGEABLE @
dfed9a393bTwo cold, review-only adversarial passes over this diff (a different model-context each time):
BuildPlayoutHandlerpattern) and the enqueue moved toCancellationToken.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 bothBuildPlayoutHandlercall sites; negative controls non-vacuous) → MERGEABLE.1793 tests green (9 new).
CI red is an unrelated flaky SPA test — needs an operator job re-run
Build & test (.NET)(run 960 / job 5342) failed onweb/src/screens/LibrariesScreen.test.tsx:292— "stops scan polling and refreshes sources once when scans complete" — awaitForthat timed out expecting the 75% progress bar to be removed. This is the known CI-VMwaitFor-timeout flake class (see MEMORYCI VM test timeouts).It is not this PR:
web/files (C# + docs only) —git diff --name-only origin/main...HEAD -- web/is empty.LibrariesScreen.test.tsxpasses 3/3 locally (22/22 tests each run).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 @ dfed9a39posted).