diff --git a/lib/streaming_providers/providers/rtlplus/auth.py b/lib/streaming_providers/providers/rtlplus/auth.py index 7b1b29a..bdab9e0 100644 --- a/lib/streaming_providers/providers/rtlplus/auth.py +++ b/lib/streaming_providers/providers/rtlplus/auth.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/lib/streaming_providers/providers/rtlplus/constants.py b/lib/streaming_providers/providers/rtlplus/constants.py index c69ba01..cd66170 100644 --- a/lib/streaming_providers/providers/rtlplus/constants.py +++ b/lib/streaming_providers/providers/rtlplus/constants.py @@ -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: diff --git a/lib/streaming_providers/providers/rtlplus/models.py b/lib/streaming_providers/providers/rtlplus/models.py index 6c05a76..e02527a 100644 --- a/lib/streaming_providers/providers/rtlplus/models.py +++ b/lib/streaming_providers/providers/rtlplus/models.py @@ -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+""" diff --git a/lib/streaming_providers/providers/rtlplus/provider.py b/lib/streaming_providers/providers/rtlplus/provider.py index 774b339..c47bfe9 100644 --- a/lib/streaming_providers/providers/rtlplus/provider.py +++ b/lib/streaming_providers/providers/rtlplus/provider.py @@ -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 \ No newline at end of file