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

resources/lib/itvx.py — ITVX auth + live/VOD resolver

Path /resources/lib/itvx.py Lines 410 Type OAuth client + playback resolver (DASH + Widevine) ← called by default.py:_resolve_channel, default.py:_resolve_vod, ?action=itvx_login/logout → calls ITV auth.prd.user.itv.com OAuth, magni.itv.com playlist API, setup_dash_item

Role

ITVX is the only addon provider that requires an account. This module handles OAuth password-grant auth (with refresh-token lifecycle), sticks the resulting JWT into Bearer headers, then uses it to call ITV's "magni" playlist API for both live simulcast and on-demand playlists.

ITVX auth constants

AUTH_URL = "https://auth.prd.user.itv.com/v2/auth" · REFRESH_URL = "https://auth.prd.user.itv.com/token" · SIMULCAST_URL = "https://simulcast.itv.com/playlist/itvonline/" · VOD_URL = "https://magni.itv.com/playlist/itvonline/ITV/" · lines 16-19, 228

Five different ITV-hosted endpoints:

  • AUTH_URL — initial password grant (POST).
  • REFRESH_URL — refresh-token exchange (GET with ?refresh=).
  • SIMULCAST_URL — live channel playlist (POST with WEB_REQ_DATA).
  • VOD_URL — VOD programme playlist (POST with VOD_REQ_DATA). VOD endpoint is on a different host than the live one: magni.itv.com vs. simulcast.itv.com.
SESSION_FILE = /itv_session · line 21

One JSON file holds itv_session (access + refresh JWTs), an Itv.Session cookie string, and a refreshed timestamp.

WEB_REQ_DATA · VOD_REQ_DATA · lines 33-52, 231-250

Static device description payloads POSTed to the playlist URLs. Note the differences:

  • WEB_REQ_DATA is for live simulcast — uses "platformTag": "dotcom" and "featureset": {"min": ["mpeg-dash", "widevine"], "max": ["hd", "mpeg-dash", "widevine", "inband-webvtt"]}.
  • VOD_REQ_DATA uses "platformTag": "dotcom" (browser identity). Only "dotcom" and "ctv" are accepted — all others return HTTP 400. "dotcom" applies a 720p video filter (no 1080p); "ctv" serves the full resolution ladder. Both return the same DRM entitlement from the Irdeto license server — the VMP entitlement (itvx_ctv_soft_protect_hd) is content-level, not platform-level. Audio always fails on L3 Widevine regardless of platformTag: the CDM oscillates the key between kUsable and kExpired/SystemCode5 due to VMP requirements. Video works via V4L2 hardware decode (NOSECUREDECODER) which bypasses the CDM; audio has no hardware path. Live already uses "dotcom" via WEB_REQ_DATA. When upstream IA PR #2021 lands, the CDM audio decoder interface will bypass the single-decrypt VMP check. Flat featureset: ["mpeg-dash", "widevine", "outband-webvtt", "hd", "single-track"].
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0" · line 56

Hardcoded Firefox/Linux UA for ITVX playlist calls only — different from the shared get_ua() desktop-Chrome UA. ITVX sniffs UA and rejects things that look Roku-shaped; Firefox 131 is what their own web player uses, so we mimic exactly.

Session management

_decode_jwt_payload(token) · line 96 · _is_token_expired(token, margin=1800) · _is_refresh_expired(token) · lines 111, 117

JWT base64-payload decoder. Token expiry check allows a 30-min margin (margin=1800) so we refresh before the actual expiration. Refresh-token expiry uses a 1-day margin (exp - 86400).

_get_access_token() · line 123

The session-state machine. Steps:

  1. Load the session file. If no access/refresh token, return "".
  2. If refresh-token expired → log warning, return "". User must re-login via ?action=itvx_login.
  3. If access-token expired → call _refresh_session() to get a new one. Reload from disk.
  4. Proactive 4h refresh: if the session was last refreshed > 4h ago (line 140), refresh again regardless of JWT expiry. This catches the case where ITV silently invalidates older refreshes before the JWT exp claim.
_refresh_session() · line 149

Refreshes via REFRESH_URL + ?refresh=. On success updates the session file, the refreshed timestamp, and re-builds the Itv.Session sticky cookie (line 92's _build_cookie wraps the tokens in a {"sticky": true, "tokens": {"content": …}} blob). Cookies stored alongside in the session file for itvx_vod.py's catalogue fetches.

login(email=None, password=None) · line 176

Initial login. Reads itvx_email / itvx_password addon settings if not supplied. POSTs to AUTH_URL with the password grant_type (NOT the cookies-based OAuth code flow that the website uses — ITV's API directly accepts password grant for the app.10ft.itv.com "Origin" the resolver sends in AUTH_HEADERS).

On 400 → "invalid email or password" notification; 403 → "forbidden, try again later" (rate-limit); other → "HTTP N" generic. On success extracts the user nickname from the JWT payload (_decode_jwt_payload(...).get("name", "")) and notifies "ITVX signed in as ".

logout() · line 217

Deletes the session file. UI notification "ITVX signed out".

Resolvers

resolve(channel, listitem) · line 253

Live channel playlist. Calls _get_access_token(); if that returns empty (no session), it tries login() once as a fallback (uses the saved credentials in settings → fully automatic). Then POSTs WEB_REQ_DATA with the token to SIMULCAST_URL % channel.

On HTTP 400/401/403: log "auth error N, attempting refresh", call _refresh_session(), rebuild the request, retry once. On a second failure raises "session expired - sign in again".

Reads data["Playlist"]["Video"]["VideoLocations"][0]dash_url = loc["Url"], key_service = loc["KeyServiceUrl"]. If key_service is set, we're playing DRM content — pass to setup_dash_item with the license URL + headers. Else clear DASH.

resolve_vod(production_id, listitem, playlist_url=None) · line 320

Same shape as live but against the magni VOD URL. Has an additional "version retry" fallback (lines 357-372): on a 404 from a default-constructed URL, tries .001 through .009 version suffixes — ITV's API exposes "versions" of a programme as separate URLs (e.g. signed, AD, repeat episodes), and the very first version isn't always the canonical "1". The retry finds the first one that returns 200.

playlist_url short-circuit

If the caller passes playlist_url (the URL the show dict holds, captured during catalogue scrape), we use it verbatim and skip the version retry — the catalogued URL is authoritative. This is the path most VOD episodes take; the production_id + version-suffix fallback only kicks in when the cataloguing pipeline couldn't pick up a playlist URL.

404 with playlist_url is final

If a 404 comes back with a caller-supplied playlist_url, we re-raise rather than retrying — the URL has been observed stale by the caller and we trust that signal rather than spam ITV with version probes for content we already know is gone.

Inbound routes

RouteFunctionTriggered by
?action=itvx_loginlogin()UI "Sign in" button menu action
?action=itvx_logoutlogout()UI "Sign out" button
?action=resolve_vod&provider=itvx&…resolve_vod()Play episode click in the library
?action=resolve_live&provider=itvx&…resolve(channel, item)Live channel click in Live TV