From a80aa96c6dcf50ca111a2c036d96937d80a1bca0 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Wed, 28 Oct 2026 13:49:00 +0100 Subject: [PATCH] centralize browser versions --- lib/streaming_providers/providers/globals.py | 312 ++++++++++++++---- .../providers/hrti/constants.py | 23 +- .../providers/magentaeu/constants.py | 21 +- .../providers/movetv/constants.py | 17 +- .../providers/zdf/constants.py | 14 +- 5 files changed, 296 insertions(+), 91 deletions(-) diff --git a/lib/streaming_providers/providers/globals.py b/lib/streaming_providers/providers/globals.py index a55ff93..efb9e97 100644 --- a/lib/streaming_providers/providers/globals.py +++ b/lib/streaming_providers/providers/globals.py @@ -3,12 +3,12 @@ # Global User Agent Registry # # Provides current, browser-accurate user agent strings for all providers. -# Chrome/Chromium version is fetched live from the official version API; -# other browsers track their own versioning but share the same Blink engine -# version where applicable. +# Chrome/Chromium version is fetched live from the official VersionHistory +# API per platform (linux / win / mac); other browsers track their own +# versioning but share the same Blink engine version where applicable. # # Usage: -# from providers.globals import get_user_agent +# from providers.globals import get_user_agent, get_identity # # get_user_agent("linux", "chrome") # Desktop Chrome on Linux # get_user_agent("windows", "edge") # Desktop Edge on Windows @@ -18,6 +18,9 @@ # get_user_agent("android", "dalvik") # Android system HTTP client # get_user_agent("ios", "safari") # Mobile Safari on iOS # get_user_agent("firetv", "chrome") # Fire TV (AFTS) WebView +# +# ident = get_identity("linux", "chrome") # BrowserIdentity struct +# ident.user_agent, ident.chrome_major, ident.sec_ch_ua, ... # ============================================================================ from __future__ import annotations @@ -26,6 +29,7 @@ import json import threading import urllib.error import urllib.request +from dataclasses import dataclass from functools import lru_cache from typing import Literal, Optional @@ -34,10 +38,22 @@ from ..base.utils.logger import logger # --------------------------------------------------------------------------- # Version constants — updated whenever this file is refreshed. # These serve as fallbacks when the live fetch fails. +# +# Stable versions as of 2026-10-28 (VersionHistory API): +# linux 156.0.8078.73 +# win 157.0.8103.15 (Windows channel currently ahead of the others) +# mac 156.0.8078.74 +# mac_arm64 156.0.8078.74 # --------------------------------------------------------------------------- -_CHROME_FALLBACK_VERSION = "146.0.7657.48" # Stable as of 2026-03-10 -_EDGE_FALLBACK_VERSION = "145.0.3800.97" # Stable as of 2026-03-09 +_CHROME_LINUX_FALLBACK_VERSION = "156.0.8078.73" +_CHROME_WIN_FALLBACK_VERSION = "157.0.8103.15" +_CHROME_MAC_FALLBACK_VERSION = "156.0.8078.74" + +# Retained for callers that still reference a single Chrome fallback. +_CHROME_FALLBACK_VERSION = _CHROME_LINUX_FALLBACK_VERSION + +_EDGE_FALLBACK_VERSION = "157.0.8103.15" # Edge tracks Chromium; same line as win _SAFARI_FALLBACK_VERSION = "18.3" # Safari 18.3 / WebKit 619 _WEBKIT_FALLBACK_BUILD = "605.1.15" _IOS_FALLBACK_VERSION = "18.3.2" @@ -45,27 +61,34 @@ _MACOS_FALLBACK_VERSION = "14_4" # Sonoma 14.4 _ANDROID_FALLBACK_VERSION = "13" # Google VersionHistory API — https://versionhistory.googleapis.com/v1/ -# Path structure: /v1/chrome/{platform}/channels/stable/versions -# Platform slugs: "linux" and "win". Edge is Chromium-based and ships the same -# version, so we reuse these fetches for Edge UA strings too. +# Path structure: /v1/chrome/platforms/{platform}/channels/stable/versions +# Platform slugs: "linux", "win", "mac", "mac_arm64". Edge is Chromium-based +# and ships the same version, so we reuse these fetches for Edge UA strings. # No auth required. _CHROME_LINUX_VERSION_API = ( - "https://versionhistory.googleapis.com/v1/chrome/linux" + "https://versionhistory.googleapis.com/v1/chrome/platforms/linux" "/channels/stable/versions?order_by=version+desc&pageSize=1" ) _CHROME_WIN_VERSION_API = ( - "https://versionhistory.googleapis.com/v1/chrome/win" + "https://versionhistory.googleapis.com/v1/chrome/platforms/win" + "/channels/stable/versions?order_by=version+desc&pageSize=1" +) +_CHROME_MAC_VERSION_API = ( + "https://versionhistory.googleapis.com/v1/chrome/platforms/mac" "/channels/stable/versions?order_by=version+desc&pageSize=1" ) # Thread-safe one-time fetches — one slot per platform. -# Separate locks so linux and win fetches run concurrently at startup. +# Separate locks so linux/win/mac fetches run concurrently at startup. _linux_lock = threading.Lock() _win_lock = threading.Lock() +_mac_lock = threading.Lock() _linux_ready = threading.Event() _win_ready = threading.Event() +_mac_ready = threading.Event() _linux_version: Optional[str] = None _win_version: Optional[str] = None +_mac_version: Optional[str] = None def _fetch_version_get(api_url: str, fallback: str, label: str) -> str: @@ -75,16 +98,43 @@ def _fetch_version_get(api_url: str, fallback: str, label: str) -> str: Returns the fallback constant on any network or parse error. """ try: - req = urllib.request.Request(api_url, headers={"Accept": "application/json"}) - with urllib.request.urlopen(req, timeout=4) as resp: + req = urllib.request.Request( + api_url, + headers={ + "Accept": "application/json", + # A plain Accept header gets rejected by some WAFs; a plausible + # UA keeps the endpoint reachable from restrictive networks. + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/156.0.0.0 Safari/537.36" + ), + }, + ) + with urllib.request.urlopen(req, timeout=10) as resp: data = json.loads(resp.read().decode()) - version = data["versions"][0]["version"] - logger.debug(f"Fetched live {label} version: {version}") - return version + versions = [ + v["version"] for v in data.get("versions", []) if v.get("version") + ] + if not versions: + raise ValueError("empty 'versions' array in response") + # Do not trust the server-side order_by blindly: take the numeric max. + latest = max( + versions, key=lambda v: [int(p) for p in v.split(".") if p.isdigit()] + ) + logger.debug(f"Fetched live {label} version: {latest}") + return latest + except urllib.error.HTTPError as exc: + body = exc.read().decode(errors="replace")[:300] + logger.warning( + f"Version API HTTP {exc.code} for {label} ({api_url}): {body}; " + f"using fallback {fallback}" + ) + return fallback except Exception as exc: logger.warning( - f"Could not fetch {label} version from API ({exc}); " - f"using fallback {fallback}" + f"Could not fetch {label} version from API " + f"({type(exc).__name__}: {exc}); using fallback {fallback}" ) return fallback @@ -96,7 +146,9 @@ def _fetch_linux_version() -> str: with _linux_lock: if not _linux_ready.is_set(): _linux_version = _fetch_version_get( - _CHROME_LINUX_VERSION_API, _CHROME_FALLBACK_VERSION, "chrome/linux" + _CHROME_LINUX_VERSION_API, + _CHROME_LINUX_FALLBACK_VERSION, + "chrome/linux", ) _linux_ready.set() _linux_ready.wait() @@ -110,13 +162,30 @@ def _fetch_win_version() -> str: with _win_lock: if not _win_ready.is_set(): _win_version = _fetch_version_get( - _CHROME_WIN_VERSION_API, _CHROME_FALLBACK_VERSION, "chrome/win" + _CHROME_WIN_VERSION_API, + _CHROME_WIN_FALLBACK_VERSION, + "chrome/win", ) _win_ready.set() _win_ready.wait() return _win_version # type: ignore[return-value] +def _fetch_mac_version() -> str: + """Return the latest stable Chrome/mac version, fetched exactly once.""" + global _mac_version + if not _mac_ready.is_set(): + with _mac_lock: + if not _mac_ready.is_set(): + _mac_version = _fetch_version_get( + _CHROME_MAC_VERSION_API, + _CHROME_MAC_FALLBACK_VERSION, + "chrome/mac", + ) + _mac_ready.set() + _mac_ready.wait() + return _mac_version # type: ignore[return-value] + # --------------------------------------------------------------------------- # OS token helpers @@ -138,91 +207,141 @@ def _android_token(version: str = _ANDROID_FALLBACK_VERSION) -> str: return f"Linux; Android {version}" +# --------------------------------------------------------------------------- +# Identity struct +# --------------------------------------------------------------------------- + +OS = Literal["windows", "linux", "macos", "android", "ios", "firetv", "androidtv", "androidmobile"] +UA = Literal["chrome", "edge", "safari", "dalvik", "firefox"] + + +@dataclass(frozen=True) +class BrowserIdentity: + """ + A coherent browser identity: the UA string, the client-hint values and the + version tokens all derive from the SAME fetched version, so they cannot + drift apart (a UA that says Chrome 154 next to hints that say 156 is a + classic bot signature). + """ + os: str + browser: str + chrome_major: str # "156" + chrome_full: str # "156.0.8078.73" + blink: str # "537.36" + user_agent: str + sec_ch_ua: str # '"Google Chrome";v="156", ...' + sec_ch_ua_platform: str # '"Linux"' | '"Windows"' | '"macOS"' + device_name: str # "Chrome 156" (eon / movetv style) + + +def _sec_ch_ua(major: str) -> str: + return ( + f'"Google Chrome";v="{major}", "Not_A Brand";v="8", ' + f'"Chromium";v="{major}"' + ) + + +def _sec_ch_platform(os_name: str) -> str: + return { + "windows": '"Windows"', + "macos": '"macOS"', + "linux": '"Linux"', + "android": '"Android"', + "androidtv": '"Android"', + "androidmobile": '"Android"', + "firetv": '"Android"', + "ios": '"iOS"', + }.get(os_name, '"Unknown"') + + # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- -OS = Literal["windows", "linux", "macos", "android", "ios", "firetv", "androidtv", "androidmobile"] -UA = Literal["chrome", "edge", "safari", "dalvik", "firefox"] - - @lru_cache(maxsize=None) -def get_user_agent(os: OS, browser: UA) -> str: +def get_identity(os: OS, browser: UA) -> BrowserIdentity: """ - Return a current, standards-compliant user agent string. + Return a coherent BrowserIdentity for the given (os, browser) pair. - Parameters - ---------- - os: - Target operating system / device class. - One of: "windows", "linux", "macos", "android", "ios", - "firetv", "androidtv", "androidmobile" - browser: - Browser / HTTP client. - One of: "chrome", "edge", "safari", "dalvik", "firefox" - - Returns - ------- - str - A fully formed User-Agent string. - - Raises - ------ - ValueError - When the (os, browser) combination is unsupported. + The version tokens (chrome_major / chrome_full), the UA string, the + sec-ch-ua hints and device_name are all derived from the SAME fetched + version for the target platform, so they cannot drift apart. """ - cv_linux = _fetch_linux_version() # e.g. "146.0.7657.48" - cv_win = _fetch_win_version() # e.g. "146.0.7657.48" (may differ slightly) + # Pick the platform-specific version that matches the UA token we emit. + cv_linux = _fetch_linux_version() # e.g. "156.0.8078.73" + cv_win = _fetch_win_version() # e.g. "157.0.8103.15" + cv_mac = _fetch_mac_version() # e.g. "156.0.8078.74" blink = "537.36" # frozen — all Chromium UA strings use this + def _ident(os_name: str, ua: str, full: str) -> BrowserIdentity: + major = full.split(".")[0] + return BrowserIdentity( + os=os_name, + browser=browser, + chrome_major=major, + chrome_full=full, + blink=blink, + user_agent=ua, + sec_ch_ua=_sec_ch_ua(major), + sec_ch_ua_platform=_sec_ch_platform(os_name), + device_name=f"Chrome {major}", + ) + # ------------------------------------------------------------------ # Chrome on desktop # ------------------------------------------------------------------ if browser == "chrome": if os == "linux": - return ( + ua = ( f"Mozilla/5.0 ({_linux_token()}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_linux} Safari/{blink}" ) + return _ident("linux", ua, cv_linux) if os == "windows": - return ( + ua = ( f"Mozilla/5.0 ({_win_token()}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_win} Safari/{blink}" ) + return _ident("windows", ua, cv_win) if os == "macos": - return ( + ua = ( f"Mozilla/5.0 ({_macos_token()}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " - f"Chrome/{cv_win} Safari/{blink}" + f"Chrome/{cv_mac} Safari/{blink}" ) + return _ident("macos", ua, cv_mac) if os == "android": av = _ANDROID_FALLBACK_VERSION - return ( + ua = ( f"Mozilla/5.0 ({_android_token(av)}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_linux} Mobile Safari/{blink}" ) + return _ident("android", ua, cv_linux) if os == "androidmobile": av = _ANDROID_FALLBACK_VERSION - return ( + ua = ( f"Mozilla/5.0 (Linux; Android {av}; SM-G991B) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_linux} Mobile Safari/{blink}" ) + return _ident("androidmobile", ua, cv_linux) if os == "androidtv": - return ( + ua = ( f"Mozilla/5.0 (Linux; Android {_ANDROID_FALLBACK_VERSION}; SHIELD Android TV) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_linux} Safari/{blink}" ) + return _ident("androidtv", ua, cv_linux) if os == "firetv": - return ( + ua = ( f"Mozilla/5.0 (Linux; Android {_ANDROID_FALLBACK_VERSION}; AFTS Build/PPR1.180610.011) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_linux} Safari/{blink}" ) + return _ident("firetv", ua, cv_linux) # ------------------------------------------------------------------ # Microsoft Edge (Chromium-based) — reuses Chrome versions from the @@ -230,26 +349,29 @@ def get_user_agent(os: OS, browser: UA) -> str: # ------------------------------------------------------------------ if browser == "edge": if os == "windows": - return ( + ua = ( f"Mozilla/5.0 ({_win_token()}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_win} Safari/{blink} " f"Edg/{cv_win}" ) + return _ident("windows", ua, cv_win) if os == "linux": - return ( + ua = ( f"Mozilla/5.0 ({_linux_token()}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " f"Chrome/{cv_linux} Safari/{blink} " f"Edg/{cv_linux}" ) + return _ident("linux", ua, cv_linux) if os == "macos": - return ( + ua = ( f"Mozilla/5.0 ({_macos_token()}) " f"AppleWebKit/{blink} (KHTML, like Gecko) " - f"Chrome/{cv_win} Safari/{blink} " - f"Edg/{cv_win}" + f"Chrome/{cv_mac} Safari/{blink} " + f"Edg/{cv_mac}" ) + return _ident("macos", ua, cv_mac) # ------------------------------------------------------------------ # Apple Safari @@ -258,65 +380,115 @@ def get_user_agent(os: OS, browser: UA) -> str: if os == "macos": sv = _SAFARI_FALLBACK_VERSION # "18.3" wk = _WEBKIT_FALLBACK_BUILD # "605.1.15" - return ( + ua = ( f"Mozilla/5.0 ({_macos_token()}) " f"AppleWebKit/{wk} (KHTML, like Gecko) " f"Version/{sv} Safari/{wk}" ) + return BrowserIdentity( + os="macos", + browser="safari", + chrome_major="", # Safari has no Chrome version + chrome_full="", + blink=wk, + user_agent=ua, + sec_ch_ua="", + sec_ch_ua_platform='"macOS"', + device_name=f"Safari {sv}", + ) if os == "ios": ios = _IOS_FALLBACK_VERSION # "18.3.2" ios_ua = ios.replace(".", "_") - sv = _SAFARI_FALLBACK_VERSION - wk = _WEBKIT_FALLBACK_BUILD - return ( + sv = _SAFARI_FALLBACK_VERSION + wk = _WEBKIT_FALLBACK_BUILD + ua = ( f"Mozilla/5.0 (iPhone; CPU iPhone OS {ios_ua} like Mac OS X) " f"AppleWebKit/{wk} (KHTML, like Gecko) " f"Version/{sv} Mobile/15E148 Safari/{wk}" ) + return BrowserIdentity( + os="ios", + browser="safari", + chrome_major="", + chrome_full="", + blink=wk, + user_agent=ua, + sec_ch_ua="", + sec_ch_ua_platform='"iOS"', + device_name=f"Safari {sv}", + ) # ------------------------------------------------------------------ # Android Dalvik (system HTTP client — used by native Android apps) # ------------------------------------------------------------------ if browser == "dalvik": if os in ("android", "androidtv", "androidmobile", "firetv"): - # e.g. Dalvik/2.1.0 (Linux; U; Android 11; SHIELD Android TV …) build = "RQ1A.210105.003" - return ( + ua = ( f"Dalvik/2.1.0 (Linux; U; Android {_ANDROID_FALLBACK_VERSION}; " f"SHIELD Android TV Build/{build})" ) + return BrowserIdentity( + os=os, + browser="dalvik", + chrome_major="", + chrome_full="", + blink="", + user_agent=ua, + sec_ch_ua="", + sec_ch_ua_platform=_sec_ch_platform(os), + device_name="Dalvik", + ) raise ValueError( f"Unsupported (os={os!r}, browser={browser!r}) combination. " - f"Supported OS values: windows, linux, macos, android, androidtv, androidmobile, ios, firetv. " + f"Supported OS values: windows, linux, macos, android, androidtv, " + f"androidmobile, ios, firetv. " f"Supported browser values: chrome, edge, safari, dalvik." ) +@lru_cache(maxsize=None) +def get_user_agent(os: OS, browser: UA) -> str: + """ + Return a current, standards-compliant user agent string. + + Thin wrapper around get_identity(); kept for existing call sites and for + providers that only need the UA string. + """ + return get_identity(os, browser).user_agent + + def invalidate_version_cache() -> None: """ Force a re-fetch of all versions on next call. Useful for long-running processes that want to pick up a new release. """ - global _linux_version, _win_version + global _linux_version, _win_version, _mac_version with _linux_lock: _linux_version = None _linux_ready.clear() with _win_lock: _win_version = None _win_ready.clear() + with _mac_lock: + _mac_version = None + _mac_ready.clear() + get_identity.cache_clear() get_user_agent.cache_clear() # --------------------------------------------------------------------------- # Eager background prefetch — runs once at import time. -# Both platform fetches run concurrently (daemon threads, never block shutdown). +# All three platform fetches run concurrently (daemon threads, never block +# shutdown). # --------------------------------------------------------------------------- def _prefetch_all() -> None: for target, name in ( (_fetch_linux_version, "prefetch-chrome-linux"), (_fetch_win_version, "prefetch-chrome-win"), + (_fetch_mac_version, "prefetch-chrome-mac"), ): threading.Thread(target=target, daemon=True, name=name).start() diff --git a/lib/streaming_providers/providers/hrti/constants.py b/lib/streaming_providers/providers/hrti/constants.py index 6f2f559..c1b47ff 100644 --- a/lib/streaming_providers/providers/hrti/constants.py +++ b/lib/streaming_providers/providers/hrti/constants.py @@ -22,6 +22,13 @@ from typing import Any, ClassVar, Dict, Optional from ...base.errors import ConfigurationError from ...base.utils.logger import logger +from ..globals import get_identity + + +# One browser identity -> User-Agent AND client type, from the central +# registry (no drift between the two, and the Chrome version follows the +# live stable feed rather than a hardcoded literal). +_HRTI_IDENTITY = get_identity("linux", "chrome") class HRTiDefaults: @@ -75,13 +82,15 @@ class HRTiDefaults: APPLICATION_VERSION = "5.97.6" # replaced by env.json's applicationVersion OS_VERSION = "Linux" - # One browser version -> User-Agent AND client type (no drift between them) - CHROME_MAJOR = 142 + # One browser identity -> User-Agent AND client type (no drift between + # them). Both now follow the central Chrome registry. VERIFY: if the HRTi + # backend cross-checks client_type against the User-Agent's Chrome version + # on login, the two still match; if it expects a *fixed* historic version + # (e.g. the production web app's pinned build), a login rejection will + # appear on the first request after deploy — watch the login logs. + CHROME_MAJOR = _HRTI_IDENTITY.chrome_major CLIENT_TYPE = f"Chrome {CHROME_MAJOR}" - USER_AGENT = ( - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " - f"Chrome/{CHROME_MAJOR}.0.0.0 Safari/537.36" - ) + USER_AGENT = _HRTI_IDENTITY.user_agent # Anonymous (client-credentials) access -- the deliberate fallback ANONYMOUS_USERNAME = "anonymoushrt" @@ -259,4 +268,4 @@ class HRTiConfig: } if token: headers["authorization"] = f"Client {token}" - return headers + return headers \ No newline at end of file diff --git a/lib/streaming_providers/providers/magentaeu/constants.py b/lib/streaming_providers/providers/magentaeu/constants.py index cf205da..26388db 100644 --- a/lib/streaming_providers/providers/magentaeu/constants.py +++ b/lib/streaming_providers/providers/magentaeu/constants.py @@ -8,6 +8,12 @@ import time import uuid from typing import Dict, Optional +from ..globals import get_identity + +# One browser identity for the whole module -- UA, chrome major and any +# header value that embeds the version all read from it. +_IDENTITY = get_identity("linux", "chrome") + # Supported countries SUPPORTED_COUNTRIES = ["hr", "pl", "me", "at", "hu"] @@ -79,7 +85,8 @@ MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA0EsuGonW8+y5Oq2Hopru5oFCaXEDMBrWPDkz APP_VERSION = "02.0.1470" OS = "Linux" BROWSER = "Chrome" -BROWSER_VERSION = "149" +# Derived from the central registry; no hardcoded literal to go stale. +BROWSER_VERSION = _IDENTITY.chrome_major DEVICE_NAME = f"{OS} - {BROWSER}" # ============================================================================ @@ -93,7 +100,7 @@ DEVICE_MANUFACTURER = f"{OS} x86_64" DEVICE_CONCURRENCY_PARAM = "TVSOA-restriction-unmanagedDeviceStreamLimit" # User agent configuration -USER_AGENT = f"Mozilla/5.0 (X11; {OS} x86_64) AppleWebKit/537.36 (KHTML, like Gecko) {BROWSER}/{BROWSER_VERSION}.0.0.0 Safari/537.36" +USER_AGENT = _IDENTITY.user_agent X_USER_AGENT = ( f"{DEVICE_MODEL.lower()}|{DEVICE_TYPE.lower()}|{BROWSER}-{BROWSER_VERSION}|{APP_VERSION}|1" ) @@ -269,7 +276,15 @@ def get_guest_headers(country: str, device_id: str, session_id: str) -> Dict[str "origin": get_base_url(country), "pragma": "akamai-x-cache-on,akamai-x-check-cacheable,akamai-x-get-cache-key", "priority": "u=1, i", - "sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not;A=Brand";v="24"', + # Bump ONLY the two Chrome-version tokens; keep the captured brand order + # and the GREASE brand ("Not;A=Brand";v="24") exactly as observed. This + # string is not assembled from globals -- it is a browser capture, and a + # WAF that parses sec-ch-ua order would reject a reassembled value. + "sec-ch-ua": ( + f'"Google Chrome";v="{BROWSER_VERSION}", ' + f'"Chromium";v="{BROWSER_VERSION}", ' + f'"Not;A=Brand";v="24"' + ), "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Linux"', "sec-fetch-dest": "empty", diff --git a/lib/streaming_providers/providers/movetv/constants.py b/lib/streaming_providers/providers/movetv/constants.py index 2de3f53..0a4a66b 100644 --- a/lib/streaming_providers/providers/movetv/constants.py +++ b/lib/streaming_providers/providers/movetv/constants.py @@ -1,6 +1,8 @@ # streaming_providers/providers/movetv/constants.py from typing import Dict, Optional +from ..globals import get_identity + class MoveTVConfig: """ @@ -58,7 +60,12 @@ class MoveTVConfig: # ------------------------------------------------------------------------- PARTNER_ID: int = 2 DEVICE_MODEL_ID: int = 10 - DEVICE_NAME: str = "Chrome 155" + # VERIFY: DEVICE_NAME goes into the login payload. It was previously the + # literal "Chrome 155"; now it follows the central identity. If the MTS-SI + # backend ever validates DEVICE_NAME against a captured client string, a + # login rejection will show up immediately on the first request — watch + # the login logs after shipping this. + DEVICE_NAME: str = get_identity("macos", "chrome").device_name APP_VERSION: str = "3.5.41" # ------------------------------------------------------------------------- @@ -69,11 +76,9 @@ class MoveTVConfig: # ------------------------------------------------------------------------- # HTTP / request defaults # ------------------------------------------------------------------------- - USER_AGENT: str = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/155.0.0.0 Safari/537.36" - ) + # UA is derived from the central registry; the macOS Chrome pair matches + # the previous literal's platform, only the version is now live. + USER_AGENT: str = get_identity("macos", "chrome").user_agent TIMEOUT: int = 30 # ------------------------------------------------------------------------- diff --git a/lib/streaming_providers/providers/zdf/constants.py b/lib/streaming_providers/providers/zdf/constants.py index 9c769b1..1863a1b 100644 --- a/lib/streaming_providers/providers/zdf/constants.py +++ b/lib/streaming_providers/providers/zdf/constants.py @@ -24,6 +24,8 @@ stop working when ZDF rotates them). Override via the provider config dict import json from typing import Optional +from ..globals import get_identity + class ZdfDefaults: PROVIDER_NAME = "zdf" @@ -86,10 +88,12 @@ class ZdfDefaults: APP_ID = "ffw-mt-web-37e8d3ae" # --- Headers ------------------------------------------------------- - USER_AGENT = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " - "(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36" - ) + # UA is derived from the central registry so it cannot go stale. ZDF's + # page / GraphQL / PTMD endpoints only read the UA as a plausibility + # signal (no sec-ch-ua checks observed), so macOS Chrome is a safe + # impersonation target. VERIFY: re-confirm no WAF appeared if this + # provider ever starts failing on the page fetch. + USER_AGENT = get_identity("macos", "chrome").user_agent ACCEPT_LIST = "application/graphql-response+json,application/json;q=0.9" ACCEPT_PTMD = "application/vnd.de.zdf.v1.0+json" ACCEPT_LANGUAGE = "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7" @@ -221,4 +225,4 @@ class ZdfConfig: "User-Agent": self.user_agent, "Origin": ZdfDefaults.ORIGIN, "Referer": ZdfDefaults.REFERER, - } + } \ No newline at end of file