Disclaimer: This documentation is provided for convenience and may contain errors. Always verify information against the official Kodi and provider documentation. Report issues.
Fastmail
Home / Files / itvx_vod.py

easyplaytv-service/…/providers/itvx_vod.py — ITVX catalogue

Moved: now lives in the standalone service runtime

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.

Path easyplaytv-service/src/easyplaytv_service/vod_sync/providers/itvx_vod.py Lines 324 Type catalogue scraper (Next.js __NEXT_DATA__ JSON + session cookies) ← called by library_sync.py via _provider_module("itvx_vod"), providers/__init__.py list_shows("films") → calls www.itv.com Next.js-rendered HTML pages, ITVX's same auth/cookie session used by itvx.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

BASE_URL = "https://www.itv.com" · line 23

Root for all ITVX URLs (the new site consolidated watch.itv.com under www.itv.com).

CATEGORIES = [("drama-soaps","Drama & Soaps"), ("films","Films"), ...] · lines 25-35

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.

NEXT_DATA_RE = re.compile(r'(.+?)') · lines 37-40

Matches the embedded JSON. Uses lazy (.+?) because the JSON is single-line; the closing tag is the only valid terminator.

UA = "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0" · line 42

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.

IMG_PROPS_THUMB = {"treatment":"title", "aspect_ratio":"16x9", ...} · lines 46-58

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

_get_session() · line 64

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.

_has_consent_cookies(s) · line 101 · _set_consent_cookies(s) · line 108 · _save_cookies(s) · line 167

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

list_categories() · line 204

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.

list_shows(category_id) · line 218

Fetches https://www.itv.com/watch/categories//all, 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.

list_episodes(programme_slug, programme_id) · line 267

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).

Pagination

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.