Skip to content

Dispatcharr: use the windowed /api/epg/grid/ endpoint instead of /api/epg/programs/ for full EPG loads #157

Description

@Drvolks

Context

Dispatcharr's EPG grid endpoint now supports caller-selected time windows. The server-side implementation lives in apps/epg/api_grid.py on dev; the web guide consumes it in Dispatcharr/Dispatcharr#1645 (frontend-only — sliding window, 12h chunk prefetch, merge by program id).

Current state in StreamClient

We already use /api/epg/grid/ for the fast first paint (DispatcherClient.getFastListings, DispatcherClient.swift:1126), but phase 2 of the load uses a different, weaker source:

  • getAllListingsGET /api/epg/programs/?page_size=50000, paging up to 50 pages (DispatcherClient.swift:1157), then filtering client-side by tvg_id / epg_data_id.
  • EPGCache.startBackgroundFullLoad (EPGCache.swift:153) merges that into the fast window.
  • EPGCache.ensureDay(_:) (EPGCache.swift:298) falls back to the same full fetch when the background load failed or was cancelled.
  • LiveProgramFetcher.swift:349 uses the same endpoint for its own refreshes.

New grid API contract

GET /api/epg/grid/:

  • start / end — ISO 8601 datetime (or bare date → midnight; naive treated as UTC). Either may be omitted: start defaults to now−1h, end defaults to start+24h. Takes precedence over days/prev_days.
  • days (1–365, clamped) / prev_days (0–30, clamped) — relative offsets from now.
  • channel_profile_id — int, or all/empty for the caller's default union.
  • No params → the historical now−1h → now+24h window, so our existing call is unchanged.
  • Max span 395 days. end <= start, bad ints, or unparseable datetimes → non-streaming 400 {"error": ...}.
  • Response keeps the {"data":[...]} envelope, so EPGGridResponse decodes as-is.

Why this is more than a bandwidth optimization

The grid endpoint and /api/epg/programs/ are not equivalent data sources, and we're using the weaker one for the full load:

  1. Dummy programmes are missing past +24h. Grid generates programmes on demand for channels with a dummy EPG source or no EPG at all (_iter_dummy_for_channels). Those rows do not exist in ProgramData, so our phase-2 merge can never fetch them — channels without real EPG go blank as soon as the user scrolls past the fast window. User-visible bug.
  2. EPG overrides are ignored. Grid resolves each channel's effective EPG assignment (effective_epg_data_obj, override wins). We match raw rows ourselves via tvgIdToChannelIds.
  3. We download invisible channels. Grid applies hidden_from_output, user_level, adult-content hide, and the assigned-profile union, and drops orphan EPG rows. /api/epg/programs/ returns everything in the DB and we filter client-side.
  4. Payload size. Up to 50 pages × 50k programs per cold load, vs. bounded windows.

Scope

  • Add start/end (and channel_profile_id) to the grid call; keep the no-param call as the compatibility path for older servers. Needs a support-detection strategy — an older server ignores unknown params silently and returns 25h, so we can't assume the window we asked for is the window we got.
  • Repoint getAllListings from /api/epg/programs/?page_size=50000 to windowed grid calls, passing the profile the guide is currently showing.
  • Make EPGCache.ensureDay(_:) fetch just that day's window instead of refetching the entire EPG.
  • Consider chunked prefetch like the web guide (12h chunks, merge by id) rather than one large multi-day fetch. startBackgroundFullLoad already merges by program id. Caveat: dummy-programme ids come from abs(raw.hashValue) (DispatcharrProgram.swift:52), which is process-seeded — fine for in-session dedup, don't build persistence on it.
  • Retire /api/epg/programs/ to a legacy fallback; same for LiveProgramFetcher.swift:349.
  • Handle truncated streams: grid is now a StreamingHttpResponse (chunked, no Content-Length), so an exception mid-stream yields a truncated body under a 200. Decode failure must degrade to "keep what we have," not a hard error.
  • Refresh the vendored DispatcherPVR/dispatcharr-api-spec.yaml — line 2822 still documents /api/epg/grid/ with parameters: [] and the hardcoded 24h description.

Out of scope / follow-up

The per-channel GET /api/epg/epgdata/{id}/ fan-out (DispatcherClient.swift:923 and :1030) — one request per channel with an epg_data_id, 20 concurrent, run on both getChannels() and getChannelSummary() just to resolve tvg_id mismatches. Worth asking upstream to inline that mapping in the channel serializer or expose a bulk epgdata list. Nothing in #1645 touches it.

The XMLTV path (/output/epg, DispatcherClient.swift:2022) is unaffected — it is only reached when JWT auth fails and we fall back to Xtream-Codes (:610), where no authenticated API is available.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions