feat(DispatcherClient): add XC Catch-up (Timeshift) session API (#119) — model + service + mock-server - #130
Draft
Drvolks wants to merge 1 commit into
Draft
feat(DispatcherClient): add XC Catch-up (Timeshift) session API (#119) — model + service + mock-server#130Drvolks wants to merge 1 commit into
Drvolks wants to merge 1 commit into
Conversation
Implements the Dispatcharr /api/catchup/sessions/ contract from issue #119 (Dispatcharr commit 223dff33). Mints a playback session, resolves the relative playback_url, and revokes on dispose. Scope of this PR (model + service + client method + mock-server — fully Linux-testable + Xcode-validatable): - New CatchupSessionRequest Codable (channel_uuid + start in snake_case, matching Dispatcharr wire format) - New CatchupSessionCreateResponse Codable (session_id + playback_url + expires_at + echo fields) - New CatchupService actor wrapping DispatcherClient with URL resolution, the 60s/600s TTL constants pinned for the spec, and actor isolation for parallel mint+revoke - DispatcherClient.startCatchupSession() and endCatchupSession() matching the same auth + demo + useOutputEndpoints guards as the rest of the client - mock-server-dispatcharr POST/DELETE /api/catchup/sessions/ with the same response shape and the same error codes (400 missing fields, 400 no catch-up streams, 404 unknown channel, 404 unknown session, 503 playback) - 8 unit tests covering encode/decode round-trips, snake_case wire format pins, Unix-epoch start fallback, unknown-field tolerance, and the TTL constants Linux validation: swift test passes 147/147 on Debian 13 / Swift 6.0.3 (139 model-layer tests from #112 + 8 new CatchupSessionTests). Mock-server validation: end-to-end curl loop verified all five response codes (201 mint, 204 delete, 404 delete-twice, 400 missing-fields, 404 unknown-channel, 503 playback) match the spec. Follow-up PRs (out of scope here, require Xcode / SwiftUI): - PlayerView: wire CatchupService.startSession into the archived-programme picker, open the playback URL within the 60s HANDSHAKE_TTL, call endSession on dispose - GuideView: surface a date-scroller on ProgrammeDetailView so users can pick past programmes (open question #1 from the pinned spec comment) - AppState: track active session_id so a backgrounded-then- resumed player can DELETE explicitly rather than waiting for idle TTL - DispatcharrBackendSettings: add the catch-up toggle row for per-channel is_catchup filtering All test fixture UUIDs are nil-UUIDs (00000000-...-0000) — no real user or server data.
Drvolks
marked this pull request as draft
August 13, 2026 20:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Dispatcharr
/api/catchup/sessions/contract from issue #119's pinned spec (Dispatcharr commit 223dff33). StreamClient can mint a catch-up playback session, open the relativeplayback_urlin MPV, and revoke on dispose — same lifecycle as live TV but with the two TTLs (60 s handshake / 600 s idle sliding) surfaced for UX.What's in this PR
NexusPVR/Core/Models/CatchupSession.swift— twoCodablestructs:CatchupSessionRequest(channel_uuid+startsnake-case, sent toPOST)CatchupSessionCreateResponse(session_id+playback_url+expires_at+ echoes, returned fromPOST)NexusPVR/Core/Services/CatchupService.swift— actor that wrapsDispatcherClientwith:baseURL + relative→ absoluteURLfor mpv)NexusPVR/Core/Services/DispatcherClient.swift—startCatchupSession()andendCatchupSession()methods on the existing client, with the same auth + demo +useOutputEndpointsguards as the rest of/api/. Mint POSTs to/api/catchup/sessions/, revoke DELETEs/api/catchup/sessions/{id}/.NexusPVRTests/CatchupSessionTests.swift— 8 tests:requestEncodesSnakeCaserequestRoundTriprequestDecodesUnixEpoch(Dispatcharr accepts both ISO-8601 and Unix epoch forstart)responseDecodesCanonicalresponseRoundTripresponseWireFormatPinsSnakeCaseresponseIgnoresUnknownFields(forward-compat)ttlConstantsMatchSpec(regression guard for the 60 s / 600 s constants)mock-server-dispatcharr/server.js— adds three new routes:POST /api/catchup/sessions/(201 with the documented response shape; 400 on missing fields or no catch-up streams; 404 on unknown channel)DELETE /api/catchup/sessions/{id}/(204 on success; 404 on unknown — matches the existence-check spec)GET /proxy/catchup/<uuid>?session_id=<id>(503, same shape as/proxy/ts/stream/)CATCHUP_SESSIONSmap keyed by the crypto-generated session ID, with a 60 s handshake TTL baked intoexpires_atWhat's NOT in this PR (out of scope, follow-ups)
PlayerView.swift/MPVPlayerCore.swift— wireCatchupService.startSession()into the archived-programme picker, open the playback URL within the 60 s handshake TTL, callendSession()on dispose. This is the user-visible piece and needs a real Apple TV for the seek/byte-range flow validation.GuideView.swift/ProgramDetailView.swift— surface a date scroller so users can pick past programmes (open question App connect icons #1 from the pinned spec). The current guide filters tonow/next.AppState.swift— track active session_id so a backgrounded-then-resumed player can DELETE explicitly rather than waiting for idle TTL.DispatcharrBackendSettings.swift— add the per-channelis_catchupfilter toggle.Issue #119 is split into two PRs along these lines so the data layer + mock-server are reviewable and Linux-validatable independently, then the SwiftUI plumbing lands in focused follow-ups that need a Mac.
Test plan
Linux (this LXC):
swift test --parallelpasses 147/147, including the 8 newCatchupSessionTests. TheDispatcherClientitself usesCombine(Apple-only) so the harness only validates the models + the actor's URL-resolution + the TTL constants. The full integration tests run via mock-server below.Mock-server end-to-end (this LXC): started
mock-server-dispatcharr/server.js --port 9292 --channels 50and exercised the routes withcurl:POST /api/catchup/sessions/(valid)GET /proxy/catchup/<uuid>?session_id=<id>DELETE /api/catchup/sessions/<id>/DELETE /api/catchup/sessions/<id>/(twice)POST /api/catchup/sessions/(missing fields)POST /api/catchup/sessions/(unknown channel_uuid)macOS (maintainer):
xcodebuild test -project NexusPVR.xcodeproj -scheme Dispatcharr— the synchronized-folder test target picks upCatchupSessionTests.swiftautomatically (nopbxprojedit needed). Themock-server-dispatcharr/server.jschanges can be smoke-tested on the Mac side by starting the mock, pointing a Dispatcharr build athttp://localhost:9191/, and confirming the player hits the new endpoints.Migration safety
The new methods are purely additive — no existing call site changed, no
pbxprojedits, no existing test broke. Users on a Dispatcharr build older than 223dff33 will get a thrownPVRClientError.invalidResponse(404 surfaced) fromstartCatchupSession(), which the UI caller is expected to render as "feature not available on this server version" — the same pattern the existinggetM3UAccounts()andgetChannelStreams()use for newer endpoints.The mock-server extension is opt-in: a maintainer running
node mock-server-dispatcharr/server.jsafter this PR lands will get the new routes; older mock-server versions will return 404 (the pre-PR behavior). No mock data is required.Author note
drvolks <drvolks@users.noreply.github.com>— the global git config, not an AI-agent identity. The #107 PR's author was wrong (hermes <hermes@local>); if the maintainer wants that amended, it'sgit commit --amend --reset-author && git push --force-with-leaseon the feat-107 branch. All commits in this PR (and #112) use the correct identity.