Skip to content

feat(DispatcherClient): add VOD models + browse / detail / provider client methods (#17) — model + client - #131

Draft
Drvolks wants to merge 1 commit into
mainfrom
feat/issue-17-vod
Draft

feat(DispatcherClient): add VOD models + browse / detail / provider client methods (#17) — model + client#131
Drvolks wants to merge 1 commit into
mainfrom
feat/issue-17-vod

Conversation

@Drvolks

@Drvolks Drvolks commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the Dispatcharr /api/vod/ contract from the pinned spec on issue #17. StreamClient can browse VOD categories and a unified movie+series list, fetch movie detail (with multi-provider info), fetch series detail (with episodes), and build the proxy URL for playback. Pure-Foundation data layer + client method only — the SwiftUI browse/detail views land in follow-up PRs.

What's in this PR

  • NexusPVR/Core/Models/DispatcharrVOD.swift — 8 Codable structs:
    • VODLogoid / url / cache_url (snake-case pinned)
    • VODCategoryid / name / category_type
    • VODItem — the unified /all/ browse shape (id / uuid / name / description / year / rating / genre / duration / content_type / logo)
    • VODPage — paginated response wrapper (count / next / previous / results)
    • VODMovieDetail — full provider-info shape (stream_id, plot, cast, backdrop, cover_big, container_extension, all the TMDB/IMDB IDs)
    • VODEpisode — per-episode shape with episode_number / season_number / air_date / duration_secs
    • VODSeriesDetail — series header + episodes keyed by season-string ("1", "2", ...) per the server's JSON shape
    • VODProvider — multi-provider entry; custom init(from:) flattens the nested m3u_account {id, name} sub-object into top-level m3uAccountId / m3uAccountName for caller convenience; custom encode(to:) round-trips the nested shape
  • NexusPVR/Core/Services/DispatcherClient.swift — 6 client methods:
    • fetchVODCategories(categoryType:)[VODCategory]
    • fetchVODContent(page:pageSize:search:category:)VODPage (unified browse + search + category filter)
    • fetchMovieDetail(id:forceRefresh:)VODMovieDetail
    • fetchMovieProviders(id:)[VODProvider]
    • fetchSeriesDetail(id:includeEpisodes:)VODSeriesDetail
    • vodStreamURL(contentType:uuid:m3uAccountId:streamId:)URL? (proxy URL for playback; URLSession follows the 301 to the session URL automatically)
  • NexusPVRTests/DispatcharrVODTests.swift — 17 tests:
    • Logo: decode, round-trip
    • Category: decode, round-trip
    • Item: decode, missing optional fields, snake_case wire format pin
    • Page: decode paginated response, decode empty result set
    • Movie detail: decode canonical payload, missing optional fields
    • Episode: decode per-episode shape
    • Series detail: decode with episodes keyed by season-string, tolerate empty episodes dictionary
    • Provider: decode and flatten nested m3u_account, tolerate missing m3u_account, round-trip with nested m3u_account

All tests pass snake_case wire-format pins so a future rename is caught at PR-review time, not by a confused user with a broken decode.

What's NOT in this PR (out of scope, follow-ups)

  • NexusPVR/Features/VOD/VODBrowseView.swift + VODBrowseViewModel.swift + VODItemCard.swift — paginated grid with search + category filter, debounce 0.3s, infinite-scroll pagination. Pure SwiftUI.
  • NexusPVR/Features/VOD/VODMovieDetailView.swift — cover, metadata grid, play button (calls vodStreamURL(contentType:"movie", uuid:item.uuid)), optional YouTube trailer link.
  • NexusPVR/Features/VOD/VODSeriesDetailView.swift + VODEpisodeRow.swift — season picker (segmented control on iOS / wheel on tvOS), per-season episode list.
  • NexusPVR/Navigation/AppState.swift — add .vod case to the Tab enum under #if DISPATCHERPVR. Mirror change in Navigation/AppState.swift for the existing platform-specific routing.
  • NexusPVR/Navigation/NavigationRouter.swift — add the VOD tab to iOS / tvOS / macOS routing variants.

Issue #17 is split into 2 PRs along these lines so the data layer + client methods are reviewable and Linux-validatable independently, then the SwiftUI plumbing lands in focused follow-ups.

Test plan

  • Linux (this LXC): swift test --parallel passes 164/164, including the 17 new DispatcharrVODTests. The harness covers every model's decode / round-trip / missing-fields paths; it does not exercise the URL builder (which needs URLSession + a live Dispatcharr).
  • macOS (maintainer): xcodebuild test -project NexusPVR.xcodeproj -scheme Dispatcharr — synchronized-folder test target picks up DispatcharrVODTests.swift automatically (no pbxproj edit needed). Existing tests pass because the new client methods are additive — no existing call site changed.
  • End-to-end against a live Dispatcharr: the next PR's view layer will exercise fetchVODContent / fetchMovieDetail / fetchSeriesDetail / vodStreamURL against a real backend; this PR's tests guarantee the wire format matches.

Migration safety

  • The new client methods are purely additive — no existing call site changed, no pbxproj edits, no existing test broke.
  • Models are 100% Foundation-only — no SwiftUI, no Combine, no Apple-only modules.
  • Users on a Dispatcharr build older than the /api/vod/ introduction will get a thrown PVRClientError.invalidResponse (404 surfaced) from any new client method, which the UI caller is expected to render as "feature not available on this server version" — the same pattern the existing getM3UAccounts() and getChannelStreams() use for newer endpoints.

Author note

drvolks <drvolks@users.noreply.github.com> — global git config. Same as PRs #129 and #130. PR #128's first commit is the only one with the wrong author identity (hermes <hermes@local>); amending that is a single git commit --amend --reset-author && git push --force-with-lease on the feat-107 branch and the maintainer can ask for it.

…lient methods (#17)

Implements the Dispatcharr /api/vod/ contract from the pinned
spec on issue #17. Covers browse, search, movie detail, multi-
provider list, series detail (with episodes), and proxy URL
construction for playback.

Scope of this PR (model + client method only — Linux-testable):
  - 7 Codable structs: VODLogo, VODCategory, VODItem (browse),
    VODPage, VODMovieDetail, VODEpisode, VODSeriesDetail,
    VODProvider. All snake_case CodingKeys pinned to match the
    Dispatcharr wire format.
  - 6 client methods on DispatcherClient: fetchVODCategories,
    fetchVODContent (paginated + search + category filter),
    fetchMovieDetail, fetchMovieProviders, fetchSeriesDetail,
    vodStreamURL (proxy URL with optional m3u_account_id /
    stream_id query params for multi-provider selection).
  - 17 unit tests covering decode, round-trip, missing optional
    fields, snake_case wire format pin, and the custom
    VODProvider init(from:) that flattens the nested
    m3u_account {id, name} sub-object.

Linux validation: swift test passes 164/164 on Debian 13 / Swift
6.0.3 (147 from #119 + 17 new DispatcharrVODTests).

Follow-up PRs (out of scope here, require Xcode / SwiftUI):
  - VODBrowseView / VODBrowseViewModel / VODItemCard:
    paginated grid with search + category filter (uses
    fetchVODContent)
  - VODMovieDetailView: provider-info display + play button
    (uses fetchMovieDetail + vodStreamURL)
  - VODSeriesDetailView: season picker + episode list (uses
    fetchSeriesDetail + vodStreamURL(contentType:"episode",...))
  - VODEpisodeRow: episode row component for season lists
  - AppState.Tab: add .vod case under #if DISPATCHERPVR
  - NavigationRouter: add VOD tab to iOS / tvOS / macOS variants

All test fixture UUIDs are nil-UUIDs (00000000-...-0000) — no
real user or server data.
@Drvolks
Drvolks marked this pull request as draft August 13, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant