Prepare RTLPlus for new streaming prov

This commit is contained in:
Nirvana
2026-04-27 11:01:52 +02:00
parent d5f708136c
commit e46d313bd8
4 changed files with 145 additions and 7 deletions
@@ -3,7 +3,7 @@ import base64
import json
import time
import hashlib
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, List
from ...base.auth.base_auth import BaseAuthToken, TokenAuthLevel
from ...base.auth.base_oauth2_auth import BaseOAuth2Authenticator
@@ -35,6 +35,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
self._bedrock_token: Optional[str] = None
self._bedrock_token_expiry: float = 0
self._cached_user_id: Optional[str] = None
self._selected_profile_id: Optional[str] = None
if proxy_config is None:
from ...base.network import ProxyConfigManager
@@ -286,7 +287,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
return token
def get_bedrock_token(self, force_refresh: bool = False) -> str:
"""Get or refresh Bedrock token."""
"""Get or refresh Bedrock token, including profile ID if available."""
if not force_refresh and self._bedrock_token and self._bedrock_token_expiry > time.time() + 300:
return self._bedrock_token
@@ -296,8 +297,11 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
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)
# Get profile ID if available
profile_id = self.get_selected_profile_id()
# Get headers with profile_id
headers = self.config.get_bedrock_token_headers(oauth_token, auth_token, timestamp, profile_id)
response = self.http_manager.get(
self.config.bedrock_auth_url,
@@ -319,6 +323,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
payload += "=" * padding
decoded = json.loads(base64.b64decode(payload))
self._bedrock_token_expiry = decoded.get("exp", 0)
logger.debug(f"Bedrock token obtained with profile: {decoded.get('profileid', 'none')}")
except Exception as e:
logger.debug(f"Could not decode Bedrock token expiry: {e}")
@@ -460,4 +465,82 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
})
if self.has_user_credentials() and hasattr(self.credentials, "username"):
status["username"] = self.credentials.username
return status
return status
def get_user_profiles(self, user_id: str = None) -> List[Dict[str, Any]]:
"""
Fetch available profiles for a user.
"""
if not user_id:
user_id = self.get_user_id_from_token()
if not user_id:
raise ValueError("No user ID available")
oauth_token = self.get_bearer_token()
bedrock_token = self.get_bedrock_token()
url = self.config.get_profiles_url(user_id)
headers = self.config.get_profiles_headers(oauth_token, bedrock_token)
response = self.http_manager.get(url, headers=headers, operation="api")
response.raise_for_status()
return response.json()
def select_profile(self, profile_id: str = None) -> bool:
"""
Select a profile to use for this session.
If no profile_id provided, fetches profiles and selects the first adult profile.
"""
if not profile_id:
user_id = self.get_user_id_from_token()
if not user_id:
logger.error("Cannot select profile: No user ID available")
return False
try:
profiles = self.get_user_profiles(user_id)
# Select first adult profile
adult_profiles = [p for p in profiles if p.get("profile_type") == "adult"]
if not adult_profiles:
logger.error("No adult profiles found")
return False
profile_id = adult_profiles[0].get("uid")
logger.info(f"Selected profile: {adult_profiles[0].get('username')} (ID: {profile_id})")
except Exception as e:
logger.error(f"Failed to fetch/select profile: {e}")
return False
# Store the selected profile ID
self._selected_profile_id = profile_id
# Save to credentials for persistence
if hasattr(self.credentials, "profile_id"):
self.credentials.profile_id = profile_id
self.save_credentials(self.credentials)
# Invalidate Bedrock token so it gets re-issued with the profile ID
self.invalidate_bedrock_token()
return True
def get_selected_profile_id(self) -> Optional[str]:
"""Get the currently selected profile ID."""
if hasattr(self, "_selected_profile_id") and self._selected_profile_id:
return self._selected_profile_id
# Try to load from stored credentials
if hasattr(self.credentials, "profile_id") and self.credentials.profile_id:
return self.credentials.profile_id
return None
def ensure_profile_selected(self) -> bool:
"""Ensure a profile is selected, auto-selecting if needed."""
if self.get_selected_profile_id():
return True
if self.has_user_credentials():
return self.select_profile()
return False
@@ -56,6 +56,8 @@ class RTLPlusDefaults:
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/"
USERS_ENDPOINT = "https://users.rtlde.bedrock.tech"
PROFILES_PATH = "/v2/platforms/m6group_web/users/{user_id}/profiles"
# DRM license server
DRMTODAY_LICENSE_URL = "https://lic.drmtoday.com/license-proxy-widevine/cenc/"
@@ -295,6 +297,7 @@ class RTLPlusHeaders:
user_agent: str,
auth_token: str,
timestamp: int,
profile_id: str = None,
) -> dict:
"""Headers for obtaining Bedrock token."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
@@ -312,6 +315,8 @@ class RTLPlusHeaders:
"x-client-release": client_version,
"x-customer-name": "rtlde",
})
if profile_id:
headers["x-auth-profile-id"] = profile_id
return headers
@staticmethod
@@ -400,6 +405,26 @@ class RTLPlusHeaders:
"X-Device-Name": RTLPlusDefaults.PLAYREADY_DEVICE_NAME,
}
@staticmethod
def get_profiles_headers(
oauth_token: str,
bedrock_token: str,
client_version: str,
user_agent: str,
) -> dict:
"""Headers for fetching user profiles."""
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-client-release": client_version,
"x-customer-name": "rtlde",
})
return headers
class RTLPlusConfig:
"""Configuration class that can be customized per instance"""
@@ -432,6 +457,8 @@ class RTLPlusConfig:
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)
self.users_endpoint = config.get("users_endpoint", RTLPlusDefaults.USERS_ENDPOINT)
self.profiles_path = config.get("profiles_path", RTLPlusDefaults.PROFILES_PATH)
# DRM license server
self.drmtoday_license_url = config.get("drmtoday_license_url", RTLPlusDefaults.DRMTODAY_LICENSE_URL)
@@ -448,6 +475,10 @@ class RTLPlusConfig:
"""Get Bedrock layout URL for a specific channel"""
return f"{self.bedrock_layout_base}/live/{channel_seo}/layout"
def get_profiles_url(self, user_id: str) -> str:
"""Get profiles URL for a specific user"""
return f"{self.users_endpoint}{self.profiles_path.format(user_id=user_id)}"
def get_epg_grid_url(self) -> str:
"""Get EPG grid URL for channel listing"""
return f"{self.bedrock_layout_base}/epg_grid"
@@ -479,7 +510,17 @@ class RTLPlusConfig:
user_agent=self.user_agent,
)
def get_bedrock_token_headers(self, oauth_token: str, auth_token: str, timestamp: int) -> dict:
def get_profiles_headers(self, oauth_token: str, bedrock_token: str) -> dict:
"""Get headers for profiles request."""
return RTLPlusHeaders.get_profiles_headers(
oauth_token=oauth_token,
bedrock_token=bedrock_token,
client_version=self.client_version,
user_agent=self.user_agent,
)
def get_bedrock_token_headers(self, oauth_token: str, auth_token: str, timestamp: int,
profile_id: str = None) -> dict:
"""Get headers for Bedrock token request."""
return RTLPlusHeaders.get_bedrock_token_headers(
oauth_token=oauth_token,
@@ -488,6 +529,7 @@ class RTLPlusConfig:
user_agent=self.user_agent,
auth_token=auth_token,
timestamp=timestamp,
profile_id=profile_id,
)
def get_bedrock_layout_headers(self, oauth_token: str, bedrock_token: str, location: str = None) -> dict:
@@ -14,13 +14,14 @@ class RTLPlusUserCredentials(UserPasswordCredentials):
RTL+ specific username/password credentials
"""
def __init__(self, username: str, password: str, client_id: Optional[str] = None):
def __init__(self, username: str, password: str, client_id: Optional[str] = None, profile_id: Optional[str] = None):
super().__init__(
username=username,
password=password,
client_id=client_id or RTLPlusDefaults.CLIENT_ID,
grant_type="password",
)
self.profile_id = profile_id # Add this attribute
def to_auth_payload(self) -> Dict[str, Any]:
"""Convert to authentication payload for RTL+"""
@@ -61,6 +61,11 @@ class RTLPlusProvider(StreamingProvider):
try:
self.bearer_token = self.authenticator.get_bearer_token()
logger.debug("RTL+ authentication successful during initialization")
# Ensure a profile is selected for user-authenticated sessions
if self.authenticator.has_user_credentials():
self.authenticator.ensure_profile_selected()
except Exception as e:
logger.warning(f"RTL+ could not authenticate during initialization: {e}")
self.bearer_token = None
@@ -363,13 +368,20 @@ class RTLPlusProvider(StreamingProvider):
def get_user_bearer_token(self) -> Optional[str]:
"""Get a user-authenticated bearer token, upgrading if necessary. Returns None if impossible."""
from ...base.auth.base_auth import TokenAuthLevel
current_level = self.authenticator.get_current_token_level()
logger.debug(
f"get_user_bearer_token: current_level={current_level}, has_user_credentials={self.authenticator.has_user_credentials()}")
if current_level == TokenAuthLevel.USER_AUTHENTICATED:
# Ensure profile is selected
self.authenticator.ensure_profile_selected()
return self.authenticator.get_bearer_token()
if self.authenticator.has_user_credentials():
token = self.authenticator.get_bearer_token(force_upgrade=True)
if self.authenticator.get_current_token_level() == TokenAuthLevel.USER_AUTHENTICATED:
self.authenticator.ensure_profile_selected()
return token
return None