easyplaytv-service/…/providers/itvx_vod.py — ITVX catalogue
Since the 3-package split this catalogue scraper ships in the shared runtime at easyplaytv-service/src/easyplaytv_service/vod_sync/providers/itvx_vod.py (and the vendored copy inside service.easyplaytv). The video addon is playback-only and no longer contains an itvx_vod.py.
Role
Scrapes ITVX's server-rendered React pages (Next.js). Each page embeds
a JSON blob inside
— the same JSON Next.js uses to hydrate the client. Parses that JSON into
the addon's show/season/episode dict shape.
Constants
Root for all ITVX URLs (the new site consolidated watch.itv.com under www.itv.com).
9 paired slugs/labels. ITVX exposes more genres internally but they don't all map cleanly to the addon's UNIFIED_GENRES — only the 9 we use appear here.
Matches the embedded JSON. Uses lazy (.+?) because the JSON
is single-line; the closing tag is the only valid
terminator.
Firefox 145 (note: itvx.py uses Firefox 131 for the playlist endpoint — slightly different sniffing requirement — while the catalogue scraper uses 145. The mismatch is empirically observed to avoid some ITVX bot-detection rules that flag mismatched UAs across the same session). Would be nice to consolidate; not a priority.
ITVX's image CDN takes a JSON-encoded query string telling it how to crop,
mask, and render the source image. This dict gets URL-encoded into the
? of each image URL.
Session management
Lazy-init of a requests.Session with a custom SSL adapter
(CustomAdapter) that pins ssl.create_default_context()
— avoids Kodi 21's / system's lenient default that breaks ITVX's cert chain.
Mounts the adapter on https://, sets Firefox 145 UA + ITVX
Origin/Referer headers, and loads cookies from a pickled
itvx_cookies.pkl (path from get_cookie_dir(),
imported from providers/__init__.py). If the saved cookies
carry consent (SyrenisCookieFormConsent*), the session is
reused as-is; otherwise consent cookies are re-issued and saved.
Consent trio. _has_consent_cookies scans the session jar for
SyrenisCookieFormConsent cookies. _set_consent_cookies
posts a CookieForm prebanner_reject_all to the Cassie consent
API (cscript-irl.cassiecloud.com/cookiesapi/submit), then sets
the SyrenisGuid_*, SyrenisCookieFormConsent_*,
Itv.Cid, Itv.Region and
Itv.ParentalControls cookies on .itv.com.
_save_cookies pickles the jar back to the cookie file for the
next run. All three swallow exceptions — a failed consent round-trip should
never block catalogue scraping.
Listing functions
Static return from CATEGORIES. Could call the live site's
/categories endpoint but ITVX's category page doesn't surface the
list-with-counts at the API level we use; the static list is simpler and
rarely changes.
Fetches https://www.itv.com/watch/categories/,
parses __NEXT_DATA__, walks
data["props"]["pageProps"]["programmes"]. Each show dict has
id (encodedProgrammeId.underscore = the production_id),
title, image (via _img_url),
description, tier, category and a slug
built by _build_slug(title). For the films category
only, it resolves the show's first episode via
_get_first_episode and marks the show standalone=True
with an episode_id + playlist_url — the sync layer
uses standalone to know not to fetch seasons. Non-200/short
responses log and return [] rather than raising.
Fetches https://www.itv.com/watch/ and parses episodes
from the same Next.js blob — walks
pageProps.seriesList[].titles[], flattening every series into one
list. Each episode dict carries id/episode_id (the
encodedEpisodeId.underscore production_id of THIS episode, used
by the resolver), title (built from Episode Title, fallback
Series X: Episode Y), subtitle (contentInfo),
series_number/episode, image, duration
and the canonical playlist_url (passed through to the resolver to
short-circuit version retry).
ITVX's catalogue page doesn't paginate at the URL level — every show for a genre returns in one __NEXT_DATA__ blob. This works fine for small genres (Comedy, News) but the Drama page is large (~2MB HTML), so a genre sync of that page is comparatively slow. Non-200 and short responses are logged and skipped rather than raising, so a failed page doesn't abort the sync.