mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-22 17:12:16 +02:00
Add discovery
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
# 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()
|
||||
@@ -3,28 +3,38 @@
|
||||
RTL+ provider constants and default configurations
|
||||
"""
|
||||
|
||||
from ..globals import get_user_agent
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
from ...base.utils.logger import logger
|
||||
|
||||
# Fetch RTL+ client version once at import time.
|
||||
_RTLPLUS_CONFIG_URL = "https://plus.rtl.de/assets/config/config.json"
|
||||
_RTLPLUS_CLIENT_VERSION_FALLBACK = "2025.6.26.0"
|
||||
try:
|
||||
with urllib.request.urlopen(_RTLPLUS_CONFIG_URL, timeout=4) as _resp:
|
||||
_RTLPLUS_CLIENT_VERSION = json.loads(_resp.read().decode())["version"]
|
||||
logger.debug(f"Fetched RTL+ client version: {_RTLPLUS_CLIENT_VERSION}")
|
||||
except Exception as _exc:
|
||||
logger.warning(f"Could not fetch RTL+ client version ({_exc}); using fallback {_RTLPLUS_CLIENT_VERSION_FALLBACK}")
|
||||
_RTLPLUS_CLIENT_VERSION = _RTLPLUS_CLIENT_VERSION_FALLBACK
|
||||
|
||||
class RTLPlusDefaults:
|
||||
"""Default values for RTL+ provider"""
|
||||
|
||||
RTLPLUS_LOGO = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f4/RTL%2B_Logo_2021.svg/2560px-RTL%2B_Logo_2021.svg.png"
|
||||
|
||||
# Client and version information
|
||||
CLIENT_VERSION = "2025.6.26.0"
|
||||
CHROME_VERSION = "121.0.0.0"
|
||||
CLIENT_ID = "rtlplus-web"
|
||||
|
||||
# Device information
|
||||
DEVICE_ID = "8c3f37cc-13a3-4141-bd0f-e4b3673fe5e4"
|
||||
DEVICE_NAME = "Linux Chrome"
|
||||
PLAYREADY_DEVICE_NAME = "Windows Edge"
|
||||
|
||||
# User Agent components
|
||||
USER_AGENT = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{CHROME_VERSION} Safari/537.36"
|
||||
|
||||
# PlayReady-specific user agent (Edge/Windows as seen in license acquisition requests)
|
||||
PLAYREADY_EDGE_VERSION = "145.0.0.0"
|
||||
PLAYREADY_USER_AGENT = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{PLAYREADY_EDGE_VERSION} Safari/537.36 Edg/{PLAYREADY_EDGE_VERSION}"
|
||||
# User agents — sourced from globals.get_user_agent() so versions stay current.
|
||||
# Windows Chrome is used for standard requests; Windows Edge for PlayReady
|
||||
# license acquisition (SOAP/XML), which requires an Edge UA.
|
||||
USER_AGENT = get_user_agent("windows", "chrome")
|
||||
PLAYREADY_USER_AGENT = get_user_agent("windows", "edge")
|
||||
|
||||
# PlayReady SOAP action for license acquisition
|
||||
PLAYREADY_SOAP_ACTION = "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"
|
||||
@@ -34,7 +44,11 @@ class RTLPlusDefaults:
|
||||
PLATFORM_ANDROID = "android"
|
||||
PLATFORM_IOS = "ios"
|
||||
PLATFORM_SMART_TV = "smarttv"
|
||||
PLATFORM_DEFAULT = PLATFORM_WEB
|
||||
PLATFORM_DEFAULT = PLATFORM_ANDROID
|
||||
|
||||
# Client and version information
|
||||
CLIENT_VERSION = _RTLPLUS_CLIENT_VERSION
|
||||
CLIENT_ID = f"rtlplus-{PLATFORM_DEFAULT}"
|
||||
|
||||
# API endpoints
|
||||
AUTH_BASE_URL = "https://auth.rtl.de/auth/realms/rtlplus/protocol/openid-connect"
|
||||
@@ -80,12 +94,12 @@ class RTLPlusDefaults:
|
||||
|
||||
# WatchPlayerConfigV3 returns stream URLs + DRM config directly from GraphQL
|
||||
# (replaces the watch-playout-variants REST endpoint for VOD)
|
||||
VOD_PLATFORM_GRAPHQL = "WEB"
|
||||
VOD_PLATFORM_GRAPHQL = PLATFORM_DEFAULT.upper()
|
||||
|
||||
# Wurstland — secondary stream resolver, returns DASH/HLS URLs
|
||||
# GET /config/{rrn}/{platform}
|
||||
VOD_WURSTLAND_CONFIG_URL = "https://wurstland.plus.rtl.de/config/{rrn}/{platform}"
|
||||
VOD_WURSTLAND_PLATFORM = "WEB"
|
||||
VOD_WURSTLAND_PLATFORM = PLATFORM_DEFAULT.upper()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VOD — root category definition
|
||||
@@ -143,6 +157,7 @@ class RTLPlusGraphQL:
|
||||
|
||||
# sha256 hashes keyed by operationName (captured from browser network traffic)
|
||||
HASHES: dict[str, str] = {
|
||||
|
||||
"LiveTvStations": "845cf56a2a78110a0f978c1a2af2bc7f9a1c937d0f324ffaf852a9a4414c8485",
|
||||
"ExploreWidgetWatch": "724f21ab86aa3f8c57673a3b346cf119f6a155bf82bb0201fd3b18e28e44f1ed",
|
||||
"TopicWorlds": "3dbde4c45532f4bdb0a1d7c210db43f0888f8a82f4aab43f7a90f3c4762d8ff7",
|
||||
@@ -202,8 +217,7 @@ class RTLPlusGraphQL:
|
||||
|
||||
@classmethod
|
||||
def topic_worlds(cls, take: int = 100, offset: int = 0) -> dict:
|
||||
"""
|
||||
TopicWorlds — browse all genre/topic worlds.
|
||||
"""TopicWorlds — browse all genre/topic worlds.
|
||||
|
||||
Confirmed from live traffic:
|
||||
response key: data.topicWorldsV2.elements[] (NOT topicWorlds.items[])
|
||||
@@ -275,11 +289,6 @@ class RTLPlusHeaders:
|
||||
return {
|
||||
"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT,
|
||||
"Accept": "application/json",
|
||||
# 'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',
|
||||
# 'Accept-Encoding': 'gzip, deflate, br',
|
||||
# 'DNT': '1',
|
||||
# 'Connection': 'keep-alive',
|
||||
# 'Upgrade-Insecure-Requests': '1'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -307,7 +316,7 @@ class RTLPlusHeaders:
|
||||
headers.update(
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"Rtlplus-Client-Id": RTLPlusDefaults.CLIENT_ID,
|
||||
"Rtlplus-Client-Id": f"rci:rtlplus:{RTLPlusDefaults.PLATFORM_DEFAULT}",
|
||||
"Rtlplus-Referrer": "",
|
||||
"Rtlplus-Client-Version": client_version or RTLPlusDefaults.CLIENT_VERSION,
|
||||
}
|
||||
@@ -329,8 +338,7 @@ class RTLPlusHeaders:
|
||||
client_version: str = None,
|
||||
user_agent: str = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get headers for the editorial GraphQL endpoint (events / ExploreWidgetWatch).
|
||||
"""Get headers for the editorial GraphQL endpoint (events / ExploreWidgetWatch).
|
||||
|
||||
Extends the standard API headers with the optional ``Rtlplus-Profile``
|
||||
JWT that the browser sends when a user profile is active. The header
|
||||
@@ -413,7 +421,6 @@ class RTLPlusConfig:
|
||||
|
||||
# Core settings (can be overridden)
|
||||
self.client_version = config.get("client_version", RTLPlusDefaults.CLIENT_VERSION)
|
||||
self.chrome_version = config.get("chrome_version", RTLPlusDefaults.CHROME_VERSION)
|
||||
self.device_id = config.get("device_id", RTLPlusDefaults.DEVICE_ID)
|
||||
self.user_agent = config.get("user_agent", RTLPlusDefaults.USER_AGENT)
|
||||
self.platform = config.get("platform", RTLPlusDefaults.PLATFORM_DEFAULT)
|
||||
@@ -439,7 +446,6 @@ class RTLPlusConfig:
|
||||
return self.manifest_endpoint.format(
|
||||
channel_id=content_id, platform=self.platform
|
||||
)
|
||||
# episode, movie, clip — full RRN passed directly
|
||||
return self.vod_playout_endpoint.format(
|
||||
content_id=content_id, platform=self.platform
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user