Files
script.service.ultimate/lib/streaming_providers/providers/rtlplus/constants.py
T
2026-03-08 19:40:16 +01:00

302 lines
12 KiB
Python

# streaming_providers/providers/rtlplus/constants.py
"""
RTL+ provider constants and default configurations
"""
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}"
# PlayReady SOAP action for license acquisition
PLAYREADY_SOAP_ACTION = "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"
# API endpoints
AUTH_BASE_URL = "https://auth.rtl.de/auth/realms/rtlplus/protocol/openid-connect"
AUTH_ENDPOINT = f"{AUTH_BASE_URL}/token"
AUTH_AUTHORIZE_ENDPOINT = f"{AUTH_BASE_URL}/auth"
GRAPHQL_ENDPOINT = "https://cdn.gateway.now-plus-prod.aws-cbc.cloud/graphql"
MANIFEST_ENDPOINT = (
"https://stus.player.streamingtech.de/livestream/linear/{channel_id}?platform=web"
)
EVENT_MANIFEST_ENDPOINT = (
"https://stus.player.streamingtech.de/liveevent/{content_id}?platform=web"
)
BASE_WEBSITE = "https://plus.rtl.de/"
CONFIG_ENDPOINT = "https://plus.rtl.de/assets/config/config.json"
# Anonymous credentials (fallback)
ANONYMOUS_CLIENT_ID = "anonymous-user"
ANONYMOUS_CLIENT_SECRET = "4bfeb73f-1c4a-4e9f-a7fa-96aa1ad3d94c"
# HTTP settings
DEFAULT_TIMEOUT = 30
# GraphQL query parameters — live TV channels
CHANNELS_QUERY_PARAMS = {
"operationName": "LiveTvStations",
"variables": '{"epgCount":4,"filter":{"channelTypes":["BROADCAST","FAST"]}}',
"extensions": '{"persistedQuery":{"version":1,"sha256Hash":"845cf56a2a78110a0f978c1a2af2bc7f9a1c937d0f324ffaf852a9a4414c8485"}}',
}
# GraphQL query parameters — editorial home view (contains LiveEventWidget nodes)
EVENTS_QUERY_PARAMS = {
"operationName": "ExploreWidgetWatch",
"variables": '{"area":"home","offset":0,"take":15}',
"extensions": '{"persistedQuery":{"version":1,"sha256Hash":"724f21ab86aa3f8c57673a3b346cf119f6a155bf82bb0201fd3b18e28e44f1ed"}}',
}
# Custom header carrying the signed profile JWT (required by the events endpoint)
PROFILE_HEADER = "Rtlplus-Profile"
# requiredPermission values returned by the events API
PERMISSION_FREE_TV = "liveeventAccessToFreeTv"
PERMISSION_PAY_TV = "liveeventAccessToPayTv"
# GraphQL __typename values used during response parsing
TYPENAME_LIVE_EVENT_WIDGET = "LiveEventWidget"
TYPENAME_LIVE_EVENT = "LiveEvent"
class RTLPlusHeaders:
"""Standard header configurations for RTL+ requests"""
@staticmethod
def get_base_headers(user_agent: str = None) -> dict:
"""Get base HTTP headers"""
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
def get_auth_headers(user_agent: str = None) -> dict:
"""Get headers for authentication requests"""
headers = RTLPlusHeaders.get_base_headers(user_agent)
headers.update(
{
"Content-Type": "application/x-www-form-urlencoded",
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
}
)
return headers
@staticmethod
def get_api_headers(
access_token: str = None,
device_id: str = None,
client_version: str = None,
user_agent: str = None,
) -> dict:
"""Get headers for authenticated API requests"""
headers = RTLPlusHeaders.get_base_headers(user_agent)
headers.update(
{
"Content-Type": "application/json",
"Rtlplus-Client-Id": RTLPlusDefaults.CLIENT_ID,
"Rtlplus-Referrer": "",
"Rtlplus-Client-Version": client_version or RTLPlusDefaults.CLIENT_VERSION,
}
)
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
if device_id:
headers["X-Device-Id"] = device_id
return headers
@staticmethod
def get_events_headers(
access_token: str,
profile_token: str = None,
device_id: str = None,
client_version: str = None,
user_agent: str = None,
) -> dict:
"""
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
is omitted when *profile_token* is ``None`` so that anonymous / client-
credential sessions work without modification.
Args:
access_token: Bearer token from the auth endpoint.
profile_token: Signed profile JWT (``Rtlplus-Profile`` header).
Pass ``None`` for anonymous / client-credential sessions.
device_id: Device UUID forwarded as ``X-Device-Id``.
client_version: Client version string; falls back to the default.
user_agent: UA string; falls back to the default.
Returns:
dict of HTTP headers ready for use with the GraphQL endpoint.
"""
headers = RTLPlusHeaders.get_api_headers(
access_token=access_token,
device_id=device_id,
client_version=client_version,
user_agent=user_agent,
)
if profile_token:
headers[RTLPlusDefaults.PROFILE_HEADER] = profile_token
return headers
@staticmethod
def get_drm_headers(access_token: str, device_id: str = None, user_agent: str = None) -> dict:
return {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "de-DE,de;q=0.9",
"Cache-Control": "no-cache",
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT,
"X-Auth-Token": access_token,
"X-Device-Id": device_id or RTLPlusDefaults.DEVICE_ID,
"X-Device-Name": RTLPlusDefaults.DEVICE_NAME,
}
@staticmethod
def get_event_manifest_headers(access_token: str, user_agent: str = None) -> dict:
"""Get headers for the event manifest endpoint (stus.player.streamingtech.de/liveevent).
Uses X-Auth-Token scheme as required by this endpoint."""
return {
"X-Auth-Token": access_token,
"Content-Type": "application/json",
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT,
}
@staticmethod
def get_playready_drm_headers(access_token: str, device_id: str = None) -> dict:
"""Get headers for PlayReady DRM license acquisition (SOAP/XML)"""
return {
"Content-Type": "text/xml; charset=UTF-8",
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
"SOAPAction": RTLPlusDefaults.PLAYREADY_SOAP_ACTION,
"User-Agent": RTLPlusDefaults.PLAYREADY_USER_AGENT,
"X-Auth-Token": access_token,
"X-Device-Id": device_id or RTLPlusDefaults.DEVICE_ID,
"X-Device-Name": RTLPlusDefaults.PLAYREADY_DEVICE_NAME,
}
class RTLPlusConfig:
"""Configuration class that can be customized per instance"""
def __init__(self, config_dict: dict = None):
"""Initialize with optional configuration overrides"""
config = config_dict or {}
self.logo = config.get("logo", RTLPlusDefaults.RTLPLUS_LOGO)
# 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)
# API endpoints (can be overridden for testing)
self.auth_endpoint = config.get("auth_endpoint", RTLPlusDefaults.AUTH_ENDPOINT)
self.graphql_endpoint = config.get("graphql_endpoint", RTLPlusDefaults.GRAPHQL_ENDPOINT)
self.manifest_endpoint = config.get("manifest_endpoint", RTLPlusDefaults.MANIFEST_ENDPOINT)
self.event_manifest_endpoint = config.get("event_manifest_endpoint", RTLPlusDefaults.EVENT_MANIFEST_ENDPOINT)
self.base_website = config.get("base_website", RTLPlusDefaults.BASE_WEBSITE)
self.config_endpoint = config.get("config_endpoint", RTLPlusDefaults.CONFIG_ENDPOINT)
# HTTP settings
self.timeout = config.get("timeout", RTLPlusDefaults.DEFAULT_TIMEOUT)
def get_manifest_url(self, content_id: str) -> str:
if "live-events" in content_id:
return self.event_manifest_endpoint.format(content_id=content_id)
return self.manifest_endpoint.format(channel_id=content_id)
def get_base_headers(self) -> dict:
"""Get base headers with this config's user agent"""
return RTLPlusHeaders.get_base_headers(self.user_agent)
def get_auth_headers(self) -> dict:
"""Get auth headers with this config's settings"""
return RTLPlusHeaders.get_auth_headers(self.user_agent)
def get_api_headers(self, access_token: str = None) -> dict:
"""Get API headers with this config's settings"""
return RTLPlusHeaders.get_api_headers(
access_token=access_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
)
def get_events_headers(self, access_token: str, profile_token: str = None) -> dict:
"""
Get headers for the editorial GraphQL endpoint with this config's settings.
Args:
access_token: Bearer token from the auth endpoint.
profile_token: Optional signed profile JWT (``Rtlplus-Profile``).
Returns:
dict of HTTP headers.
"""
return RTLPlusHeaders.get_events_headers(
access_token=access_token,
profile_token=profile_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
)
def get_drm_headers(self, access_token: str) -> dict:
"""Get DRM headers with this config's settings"""
return RTLPlusHeaders.get_drm_headers(
access_token=access_token,
device_id=self.device_id,
user_agent=self.user_agent,
)
def get_event_manifest_headers(self, access_token: str) -> dict:
"""Get event manifest headers with this config's settings."""
return RTLPlusHeaders.get_event_manifest_headers(
access_token=access_token,
user_agent=self.user_agent,
)
def get_playready_drm_headers(self, access_token: str) -> dict:
"""Get PlayReady-specific DRM headers (Edge UA, SOAP content-type)"""
return RTLPlusHeaders.get_playready_drm_headers(
access_token=access_token,
device_id=self.device_id,
)