Files
script.service.ultimate/lib/streaming_providers/providers/globals.py
T
2026-03-10 16:19:50 +01:00

324 lines
12 KiB
Python

# streaming_providers/providers/globals.py
# ============================================================================
# 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.
#
# Usage:
# from providers.globals import get_user_agent
#
# get_user_agent("linux", "chrome") # Desktop Chrome on Linux
# get_user_agent("windows", "edge") # Desktop Edge on Windows
# get_user_agent("windows", "chrome") # Desktop Chrome on Windows
# get_user_agent("macos", "safari") # Safari on macOS
# get_user_agent("android", "chrome") # Chrome for Android
# 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
# ============================================================================
from __future__ import annotations
import json
import threading
import urllib.error
import urllib.request
from functools import lru_cache
from typing import Literal, Optional
from ..base.utils.logger import logger
# ---------------------------------------------------------------------------
# Version constants — updated whenever this file is refreshed.
# These serve as fallbacks when the live fetch fails.
# ---------------------------------------------------------------------------
_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
_SAFARI_FALLBACK_VERSION = "18.3" # Safari 18.3 / WebKit 619
_WEBKIT_FALLBACK_BUILD = "605.1.15"
_IOS_FALLBACK_VERSION = "18.3.2"
_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.
# No auth required.
_CHROME_LINUX_VERSION_API = (
"https://versionhistory.googleapis.com/v1/chrome/linux"
"/channels/stable/versions?order_by=version+desc&pageSize=1"
)
_CHROME_WIN_VERSION_API = (
"https://versionhistory.googleapis.com/v1/chrome/win"
"/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.
_linux_lock = threading.Lock()
_win_lock = threading.Lock()
_linux_ready = threading.Event()
_win_ready = threading.Event()
_linux_version: Optional[str] = None
_win_version: Optional[str] = None
def _fetch_version_get(api_url: str, fallback: str, label: str) -> str:
"""
Fetch a version from the Google VersionHistory API (GET).
Response shape: {"versions": [{"version": "X.Y.Z.W", ...}]}
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:
data = json.loads(resp.read().decode())
version = data["versions"][0]["version"]
logger.debug(f"Fetched live {label} version: {version}")
return version
except Exception as exc:
logger.warning(
f"Could not fetch {label} version from API ({exc}); "
f"using fallback {fallback}"
)
return fallback
def _fetch_linux_version() -> str:
"""Return the latest stable Chrome/linux version, fetched exactly once."""
global _linux_version
if not _linux_ready.is_set():
with _linux_lock:
if not _linux_ready.is_set():
_linux_version = _fetch_version_get(
_CHROME_LINUX_VERSION_API, _CHROME_FALLBACK_VERSION, "chrome/linux"
)
_linux_ready.set()
_linux_ready.wait()
return _linux_version # type: ignore[return-value]
def _fetch_win_version() -> str:
"""Return the latest stable Chrome/win version, fetched exactly once."""
global _win_version
if not _win_ready.is_set():
with _win_lock:
if not _win_ready.is_set():
_win_version = _fetch_version_get(
_CHROME_WIN_VERSION_API, _CHROME_FALLBACK_VERSION, "chrome/win"
)
_win_ready.set()
_win_ready.wait()
return _win_version # type: ignore[return-value]
# ---------------------------------------------------------------------------
# OS token helpers
# ---------------------------------------------------------------------------
def _win_token() -> str:
return "Windows NT 10.0; Win64; x64"
def _linux_token() -> str:
return "X11; Linux x86_64"
def _macos_token() -> str:
return f"Macintosh; Intel Mac OS X {_MACOS_FALLBACK_VERSION}"
def _android_token(version: str = _ANDROID_FALLBACK_VERSION) -> str:
return f"Linux; Android {version}"
# ---------------------------------------------------------------------------
# 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:
"""
Return a current, standards-compliant user agent string.
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.
"""
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)
blink = "537.36" # frozen — all Chromium UA strings use this
# ------------------------------------------------------------------
# Chrome on desktop
# ------------------------------------------------------------------
if browser == "chrome":
if os == "linux":
return (
f"Mozilla/5.0 ({_linux_token()}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_linux} Safari/{blink}"
)
if os == "windows":
return (
f"Mozilla/5.0 ({_win_token()}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_win} Safari/{blink}"
)
if os == "macos":
return (
f"Mozilla/5.0 ({_macos_token()}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_win} Safari/{blink}"
)
if os == "android":
av = _ANDROID_FALLBACK_VERSION
return (
f"Mozilla/5.0 ({_android_token(av)}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_linux} Mobile Safari/{blink}"
)
if os == "androidmobile":
av = _ANDROID_FALLBACK_VERSION
return (
f"Mozilla/5.0 (Linux; Android {av}; SM-G991B) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_linux} Mobile Safari/{blink}"
)
if os == "androidtv":
return (
f"Mozilla/5.0 (Linux; Android {_ANDROID_FALLBACK_VERSION}; SHIELD Android TV) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_linux} Safari/{blink}"
)
if os == "firetv":
return (
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}"
)
# ------------------------------------------------------------------
# Microsoft Edge (Chromium-based) — reuses Chrome versions from the
# same VersionHistory API: win slug for Windows/macOS, linux for Linux.
# ------------------------------------------------------------------
if browser == "edge":
if os == "windows":
return (
f"Mozilla/5.0 ({_win_token()}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_win} Safari/{blink} "
f"Edg/{cv_win}"
)
if os == "linux":
return (
f"Mozilla/5.0 ({_linux_token()}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_linux} Safari/{blink} "
f"Edg/{cv_linux}"
)
if os == "macos":
return (
f"Mozilla/5.0 ({_macos_token()}) "
f"AppleWebKit/{blink} (KHTML, like Gecko) "
f"Chrome/{cv_win} Safari/{blink} "
f"Edg/{cv_win}"
)
# ------------------------------------------------------------------
# Apple Safari
# ------------------------------------------------------------------
if browser == "safari":
if os == "macos":
sv = _SAFARI_FALLBACK_VERSION # "18.3"
wk = _WEBKIT_FALLBACK_BUILD # "605.1.15"
return (
f"Mozilla/5.0 ({_macos_token()}) "
f"AppleWebKit/{wk} (KHTML, like Gecko) "
f"Version/{sv} Safari/{wk}"
)
if os == "ios":
ios = _IOS_FALLBACK_VERSION # "18.3.2"
ios_ua = ios.replace(".", "_")
sv = _SAFARI_FALLBACK_VERSION
wk = _WEBKIT_FALLBACK_BUILD
return (
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}"
)
# ------------------------------------------------------------------
# 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 (
f"Dalvik/2.1.0 (Linux; U; Android {_ANDROID_FALLBACK_VERSION}; "
f"SHIELD Android TV Build/{build})"
)
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 browser values: chrome, edge, safari, dalvik."
)
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
with _linux_lock:
_linux_version = None
_linux_ready.clear()
with _win_lock:
_win_version = None
_win_ready.clear()
get_user_agent.cache_clear()
# ---------------------------------------------------------------------------
# Eager background prefetch — runs once at import time.
# Both 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"),
):
threading.Thread(target=target, daemon=True, name=name).start()
_prefetch_all()