Prepare RTLPlus for new streaming prov

This commit is contained in:
Nirvana
2026-04-23 22:36:36 +02:00
parent f65f5985ec
commit 37475a79ea
2 changed files with 144 additions and 37 deletions
@@ -2,6 +2,7 @@
import base64
import json
import time
import hashlib
from typing import Any, Dict, Optional
from ...base.auth.base_auth import BaseAuthToken, TokenAuthLevel
@@ -215,21 +216,54 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
# Bedrock Token Management (Linear TV)
# --------------------------------------------------------------------------
def get_bedrock_token(self, force_refresh: bool = False) -> str:
"""
Get or refresh Bedrock token.
def _get_server_timestamp(self) -> int:
"""Get current server timestamp from RTL+ time endpoint."""
try:
response = self.http_manager.get(
"https://time.rtlde.bedrock.tech/",
operation="api",
headers={"User-Agent": self.config.user_agent}
)
data = response.json()
return data.get("timestamp", int(time.time()))
except Exception as e:
logger.warning(f"Could not fetch server timestamp: {e}")
return int(time.time())
The Bedrock token is a JWT required for all Bedrock API calls
(layout, DRM, heartbeat). Obtained using the OAuth token.
@staticmethod
def _generate_auth_token(device_id: str, timestamp: int) -> str:
"""
Generate the x-auth-token required for Bedrock API calls.
Based on analysis, this appears to be a SHA-1 hash of device_id + timestamp.
"""
# Convert timestamp to string
ts_str = str(timestamp)
# Try SHA-1 of device_id + timestamp (most likely)
data = f"{device_id}{ts_str}"
token = hashlib.sha1(data.encode()).hexdigest()
logger.debug(f"Generated auth token: {token} for device {device_id[:8]}... at {timestamp}")
return token
def get_bedrock_token(self, force_refresh: bool = False) -> str:
"""Get or refresh Bedrock token."""
if not force_refresh and self._bedrock_token and self._bedrock_token_expiry > time.time() + 300:
return self._bedrock_token
oauth_token = self.get_bearer_token()
# Get timestamp and generate auth token
timestamp = self._get_server_timestamp()
auth_token = self._generate_auth_token(self.config.device_id, timestamp)
# Use headers from config
headers = self.config.get_bedrock_token_headers(oauth_token, auth_token, timestamp)
response = self.http_manager.get(
self.config.bedrock_auth_url,
headers={"authorization": f"Bearer {oauth_token}"},
headers=headers,
operation="api",
)
response.raise_for_status()
@@ -21,6 +21,10 @@ class RTLPlusDefaults:
DEVICE_NAME = "Linux Chrome"
PLAYREADY_DEVICE_NAME = "Windows Edge"
# Device screen size (used in headers)
DEVICE_PLAYER_SIZE_WIDTH = 972
DEVICE_PLAYER_SIZE_HEIGHT = 919
# User agents
USER_AGENT = get_user_agent("windows", "chrome")
PLAYREADY_USER_AGENT = get_user_agent("windows", "edge")
@@ -42,6 +46,7 @@ class RTLPlusDefaults:
AUTH_AUTHORIZE_ENDPOINT = f"{AUTH_BASE_URL}/auth"
GRAPHQL_ENDPOINT = "https://cdn.gateway.now-plus-prod.aws-cbc.cloud/graphql"
BASE_WEBSITE = "https://plus.rtl.de/"
BETA_WEBSITE = "https://beta.plus.rtl.de/"
CONFIG_ENDPOINT = "https://plus.rtl.de/assets/config/config.json"
# Bedrock API endpoints (Linear TV)
@@ -49,6 +54,7 @@ class RTLPlusDefaults:
BEDROCK_LAYOUT_BASE = "https://layout.rtlde.bedrock.tech/front/v1/rtlde/m6group_web/main/token-web-31"
BEDROCK_DRM_UPFRONT_BASE = "https://drm.rtlde.bedrock.tech/v1/customers/rtlde/platforms/m6group_web/services/rtlplus_root/users/{uid}/live"
BEDROCK_HEARTBEAT_URL = "https://heartbeat-v2.rtlde.bedrock.tech/v2/platforms/m6group_web/notify/session_live"
TIME_ENDPOINT = "https://time.rtlde.bedrock.tech/"
# DRM license server
DRMTODAY_LICENSE_URL = "https://lic.drmtoday.com/license-proxy-widevine/cenc/"
@@ -141,7 +147,8 @@ def _get_rtlplus_client_version() -> str:
_CLIENT_VERSION_CACHE = data["version"]
logger.debug(f"Fetched RTL+ client version: {_CLIENT_VERSION_CACHE}")
except Exception as exc:
logger.warning(f"Could not fetch RTL+ client version ({exc}); using fallback {RTLPlusDefaults.CLIENT_VERSION_FALLBACK}")
logger.warning(
f"Could not fetch RTL+ client version ({exc}); using fallback {RTLPlusDefaults.CLIENT_VERSION_FALLBACK}")
_CLIENT_VERSION_CACHE = RTLPlusDefaults.CLIENT_VERSION_FALLBACK
return _CLIENT_VERSION_CACHE
@@ -173,7 +180,8 @@ class RTLPlusGraphQL:
@classmethod
def live_tv_stations(cls, epg_count: int = 4) -> dict:
return cls._build("LiveTvStations", f'{{"epgCount":{epg_count},"filter":{{"channelTypes":["BROADCAST","FAST"]}}}}')
return cls._build("LiveTvStations",
f'{{"epgCount":{epg_count},"filter":{{"channelTypes":["BROADCAST","FAST"]}}}}')
@classmethod
def explore_widget_watch(cls, area: str = "home", offset: int = 0, take: int = 15) -> dict:
@@ -181,7 +189,8 @@ class RTLPlusGraphQL:
@classmethod
def season_with_episodes(cls, season_rrn: str, offset: int = 0, limit: int = 48) -> dict:
return cls._build("SeasonWithFormatAndEpisodes", f'{{"seasonId":"{season_rrn}","offset":{offset},"limit":{limit}}}')
return cls._build("SeasonWithFormatAndEpisodes",
f'{{"seasonId":"{season_rrn}","offset":{offset},"limit":{limit}}}')
@classmethod
def live_events_overview_page(cls) -> dict:
@@ -205,7 +214,8 @@ class RTLPlusGraphQL:
@classmethod
def watch_player_config(cls, rrn: str) -> dict:
return cls._build("WatchPlayerConfigV3", f'{{"platform":"{RTLPlusDefaults.VOD_PLATFORM_GRAPHQL}","id":"{rrn}"}}')
return cls._build("WatchPlayerConfigV3",
f'{{"platform":"{RTLPlusDefaults.VOD_PLATFORM_GRAPHQL}","id":"{rrn}"}}')
@classmethod
def seo_url_data(cls, watch_path: str) -> dict:
@@ -228,6 +238,14 @@ class RTLPlusGraphQL:
class RTLPlusHeaders:
"""Standard header configurations for RTL+ requests"""
# Common headers shared across multiple request types
_COMMON_HEADERS = {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "de-DE,de;q=0.9",
"cache-control": "no-cache",
}
@staticmethod
def get_base_headers(user_agent: str = None) -> dict:
return {"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT, "Accept": "application/json"}
@@ -243,7 +261,8 @@ class RTLPlusHeaders:
return headers
@staticmethod
def get_api_headers(access_token: str = None, device_id: str = None, client_version: str = None, user_agent: str = None) -> dict:
def get_api_headers(access_token: str = None, device_id: str = None, client_version: str = None,
user_agent: str = None) -> dict:
headers = RTLPlusHeaders.get_base_headers(user_agent)
headers.update({
"Content-Type": "application/json",
@@ -258,69 +277,106 @@ class RTLPlusHeaders:
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:
headers = RTLPlusHeaders.get_api_headers(access_token=access_token, device_id=device_id, client_version=client_version, user_agent=user_agent)
def get_events_headers(access_token: str, profile_token: str = None, device_id: str = None,
client_version: str = None, user_agent: str = None) -> dict:
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_bedrock_layout_headers(
oauth_token: str,
bedrock_token: str,
device_id: str,
client_version: str,
user_agent: str,
location: str = None,
def get_bedrock_token_headers(
oauth_token: str,
device_id: str,
client_version: str,
user_agent: str,
auth_token: str,
timestamp: int,
) -> dict:
headers = {
"""Headers for obtaining Bedrock token."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
"authorization": f"Bearer {oauth_token}",
"cache-control": "no-cache",
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
"request-timeout": "10000",
"origin": RTLPlusDefaults.BETA_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BETA_WEBSITE,
"user-agent": user_agent,
"x-auth-device-id": device_id,
"x-auth-device-name": RTLPlusDefaults.DEVICE_NAME,
"x-auth-device-player-size-width": str(RTLPlusDefaults.DEVICE_PLAYER_SIZE_WIDTH),
"x-auth-device-player-size-height": str(RTLPlusDefaults.DEVICE_PLAYER_SIZE_HEIGHT),
"x-auth-token": auth_token,
"x-auth-token-timestamp": str(timestamp),
"x-client-release": client_version,
"x-customer-name": "rtlde",
})
return headers
@staticmethod
def get_bedrock_layout_headers(
oauth_token: str,
bedrock_token: str,
device_id: str,
client_version: str,
user_agent: str,
location: str = None,
) -> dict:
"""Headers for Bedrock layout API calls."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
"authorization": f"Bearer {oauth_token}",
"origin": RTLPlusDefaults.BETA_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BETA_WEBSITE,
"user-agent": user_agent,
"x-bedrock-token": bedrock_token,
"x-auth-device-id": device_id,
"x-auth-device-name": RTLPlusDefaults.DEVICE_NAME,
"x-client-release": client_version,
"x-customer-name": "rtlde",
}
})
if location:
headers["x-location"] = location
return headers
@staticmethod
def get_upfront_token_headers(oauth_token: str, bedrock_token: str, device_id: str, client_version: str, user_agent: str) -> dict:
return {
def get_upfront_token_headers(
oauth_token: str,
bedrock_token: str,
device_id: str,
client_version: str,
user_agent: str,
) -> dict:
"""Headers for upfront token request."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
"authorization": f"Bearer {oauth_token}",
"cache-control": "no-cache",
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
"request-timeout": "10000",
"origin": RTLPlusDefaults.BETA_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BETA_WEBSITE,
"user-agent": user_agent,
"x-bedrock-token": bedrock_token,
"x-auth-device-id": device_id,
"x-auth-device-name": RTLPlusDefaults.DEVICE_NAME,
"x-client-release": client_version,
"x-customer-name": "rtlde",
}
})
return headers
@staticmethod
def get_drm_license_headers(upfront_token: str, user_agent: str) -> dict:
return {
"accept": "*/*",
"""Headers for DRM license request to DRMToday."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
"content-type": "application/json",
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
"user-agent": user_agent,
"x-dt-auth-token": upfront_token,
}
})
return headers
@staticmethod
def get_drm_headers(access_token: str, device_id: str = None, user_agent: str = None) -> dict:
return {
"Cache-Control": "no-cache",
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT,
@@ -357,10 +413,15 @@ class RTLPlusConfig:
self.user_agent = config.get("user_agent", RTLPlusDefaults.USER_AGENT)
self.platform = config.get("platform", RTLPlusDefaults.PLATFORM_DEFAULT)
# Screen size
self.screen_width = config.get("screen_width", RTLPlusDefaults.DEVICE_PLAYER_SIZE_WIDTH)
self.screen_height = config.get("screen_height", RTLPlusDefaults.DEVICE_PLAYER_SIZE_HEIGHT)
# API endpoints
self.auth_endpoint = config.get("auth_endpoint", RTLPlusDefaults.AUTH_ENDPOINT)
self.graphql_endpoint = config.get("graphql_endpoint", RTLPlusDefaults.GRAPHQL_ENDPOINT)
self.base_website = config.get("base_website", RTLPlusDefaults.BASE_WEBSITE)
self.beta_website = config.get("beta_website", RTLPlusDefaults.BETA_WEBSITE)
self.config_endpoint = config.get("config_endpoint", RTLPlusDefaults.CONFIG_ENDPOINT)
# Bedrock endpoints (Linear TV)
@@ -368,6 +429,7 @@ class RTLPlusConfig:
self.bedrock_layout_base = config.get("bedrock_layout_base", RTLPlusDefaults.BEDROCK_LAYOUT_BASE)
self.bedrock_drm_upfront_base = config.get("bedrock_drm_upfront_base", RTLPlusDefaults.BEDROCK_DRM_UPFRONT_BASE)
self.bedrock_heartbeat_url = config.get("bedrock_heartbeat_url", RTLPlusDefaults.BEDROCK_HEARTBEAT_URL)
self.time_endpoint = config.get("time_endpoint", RTLPlusDefaults.TIME_ENDPOINT)
# DRM license server
self.drmtoday_license_url = config.get("drmtoday_license_url", RTLPlusDefaults.DRMTODAY_LICENSE_URL)
@@ -415,6 +477,17 @@ class RTLPlusConfig:
user_agent=self.user_agent,
)
def get_bedrock_token_headers(self, oauth_token: str, auth_token: str, timestamp: int) -> dict:
"""Get headers for Bedrock token request."""
return RTLPlusHeaders.get_bedrock_token_headers(
oauth_token=oauth_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
auth_token=auth_token,
timestamp=timestamp,
)
def get_bedrock_layout_headers(self, oauth_token: str, bedrock_token: str, location: str = None) -> dict:
return RTLPlusHeaders.get_bedrock_layout_headers(
oauth_token=oauth_token,