centralize browser versions

This commit is contained in:
Nirvana
2026-10-28 13:49:00 +01:00
parent 8d115399b7
commit a80aa96c6d
5 changed files with 296 additions and 91 deletions
+242 -70
View File
@@ -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()
@@ -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
@@ -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",
@@ -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
# -------------------------------------------------------------------------
@@ -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,
}
}