From 26d34dcb84290e39bac61a93634e8afc15ce2710 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Thu, 23 Apr 2026 20:55:52 +0200 Subject: [PATCH] Prepare RTLPlus for new streaming prov --- .../providers/rtlplus/auth.py | 301 ++++++------ .../providers/rtlplus/channel_manager.py | 375 +++++++++++++++ .../providers/rtlplus/constants.py | 446 ++++++++---------- .../providers/rtlplus/models.py | 67 +-- .../providers/rtlplus/provider.py | 387 +++++---------- 5 files changed, 873 insertions(+), 703 deletions(-) create mode 100644 lib/streaming_providers/providers/rtlplus/channel_manager.py diff --git a/lib/streaming_providers/providers/rtlplus/auth.py b/lib/streaming_providers/providers/rtlplus/auth.py index b47e038..1dac6ec 100644 --- a/lib/streaming_providers/providers/rtlplus/auth.py +++ b/lib/streaming_providers/providers/rtlplus/auth.py @@ -1,6 +1,7 @@ # streaming_providers/providers/rtlplus/auth.py import base64 import json +import time from typing import Any, Dict, Optional from ...base.auth.base_auth import BaseAuthToken, TokenAuthLevel @@ -13,16 +14,15 @@ from .models import RTLPlusAuthToken, RTLPlusClientCredentials, RTLPlusUserCrede class RTLPlusAuthenticator(BaseOAuth2Authenticator): def __init__( - self, - credentials=None, - config_dir=None, - client_version=None, - device_id=None, - proxy_config: Optional[ProxyConfig] = None, - http_manager=None, + self, + credentials=None, + config_dir=None, + client_version=None, + device_id=None, + proxy_config: Optional[ProxyConfig] = None, + http_manager=None, ): - # Initialize configuration FIRST config_dict = {} if client_version: config_dict["client_version"] = client_version @@ -31,33 +31,31 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): self._config = RTLPlusConfig(config_dict) self._client_id = None + self._bedrock_token: Optional[str] = None + self._bedrock_token_expiry: float = 0 + self._cached_user_id: Optional[str] = None - # Get proxy_config if not provided if proxy_config is None: from ...base.network import ProxyConfigManager proxy_mgr = ProxyConfigManager(config_dir) proxy_config = proxy_mgr.get_proxy_config("rtlplus") - # Call parent init FIRST super().__init__( provider_name="rtlplus", - credentials=credentials, # Pass None if not provided + credentials=credentials, config_dir=config_dir, proxy_config=proxy_config, http_manager=http_manager, ) - # NOW set default credentials if needed (after super init) if self.credentials is None: self.credentials = self._get_default_credentials() @property def auth_endpoint(self) -> str: - """Override auth_endpoint to use our config""" return self.config.auth_endpoint - # Required OAuth2 properties @property def oauth_client_id(self) -> str: return self._get_client_id() @@ -70,36 +68,30 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): def oauth_redirect_uri(self) -> str: return self.config.base_website + @property + def config(self) -> RTLPlusConfig: + return self._config + def _get_auth_headers(self) -> Dict[str, str]: - """RTL+-specific authentication headers""" return self.config.get_auth_headers() def _build_auth_payload(self) -> Dict[str, Any]: - """Build authentication payload from credentials""" return self.credentials.to_auth_payload() def _get_default_credentials(self): - """Get default client credentials for anonymous access""" try: - # Try to get dynamic credentials first config_creds = self._get_anonymous_credentials_from_config() if config_creds: return RTLPlusClientCredentials( client_id=config_creds.get("client_id", RTLPlusDefaults.ANONYMOUS_CLIENT_ID), - client_secret=config_creds.get( - "client_secret", RTLPlusDefaults.ANONYMOUS_CLIENT_SECRET - ), + client_secret=config_creds.get("client_secret", RTLPlusDefaults.ANONYMOUS_CLIENT_SECRET), ) except Exception as e: logger.warning(f"Could not get dynamic credentials: {e}") - # Fallback to default credentials return RTLPlusClientCredentials() def _create_token_from_response(self, response_data: Dict[str, Any]) -> RTLPlusAuthToken: - """Create RTL+-specific token from OAuth2 response""" - import time - return RTLPlusAuthToken( access_token=response_data["access_token"], token_type=response_data.get("token_type", "Bearer"), @@ -111,14 +103,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): scope=response_data.get("scope", ""), ) - def get_current_token_level(self) -> "TokenAuthLevel": - """ - Return the auth level of whichever token the base class currently holds. - - Walks the common attribute names used by BaseOAuth2Authenticator to - store the active token, classifies it via _classify_token, and returns - UNKNOWN if no token is found. - """ + def get_current_token_level(self) -> TokenAuthLevel: for attr in ("token", "_token", "current_token", "_current_token", "_access_token"): tok = getattr(self, attr, None) if tok is not None: @@ -126,36 +111,17 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): return TokenAuthLevel.UNKNOWN def get_fallback_credentials(self): - """Get fallback credentials (anonymous client credentials)""" return self._get_default_credentials() def _classify_token(self, token: BaseAuthToken) -> TokenAuthLevel: - """ - Classify RTL+ token authentication level by decoding JWT payload - - Logic: - - CLIENT_CREDENTIALS: isGuest=True AND clientId='anonymous-user' - - USER_AUTHENTICATED: Has preferred_username OR email claims - - UNKNOWN: Cannot determine or invalid token - - Args: - token: Token to classify - - Returns: - TokenAuthLevel indicating the authentication level - """ if not token or not token.access_token: - logger.debug("RTL+ Cannot classify: No token or access token") return TokenAuthLevel.UNKNOWN try: - # Decode JWT without verification to check the payload parts = token.access_token.split(".") if len(parts) < 2: - logger.debug("RTL+ Cannot classify: Invalid token format") return TokenAuthLevel.UNKNOWN - # Add padding if needed and decode payload_segment = parts[1] padding = 4 - len(payload_segment) % 4 if padding != 4: @@ -164,61 +130,38 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): payload_json = base64.b64decode(payload_segment) payload = json.loads(payload_json) - # Extract relevant claims client_id = payload.get("clientId") is_guest = payload.get("isGuest", False) preferred_username = payload.get("preferred_username") email = payload.get("email") - logger.debug( - f"RTL+ Token JWT payload: clientId={client_id}, isGuest={is_guest}, " - f"has_preferred_username={bool(preferred_username)}, has_email={bool(email)}" - ) - - # Check for user-authenticated token if preferred_username or email: - logger.debug("RTL+ Token classified as USER_AUTHENTICATED (has user claims)") return TokenAuthLevel.USER_AUTHENTICATED - # Check for client credentials (anonymous) token if is_guest and client_id == "anonymous-user": - logger.debug("RTL+ Token classified as CLIENT_CREDENTIALS (anonymous)") return TokenAuthLevel.CLIENT_CREDENTIALS - # Cannot determine - logger.debug("RTL+ Token classified as UNKNOWN (no matching criteria)") return TokenAuthLevel.UNKNOWN except Exception as e: logger.warning(f"RTL+ Error classifying token: {e}") return TokenAuthLevel.UNKNOWN - def _perform_oauth_authorization_code_flow( - self, username: str, password: str - ) -> Dict[str, Any]: - """ - RTL+ specific OAuth2 authorization code flow with PKCE - Uses base class generic form login - """ + def _perform_oauth_authorization_code_flow(self, username: str, password: str) -> Dict[str, Any]: import uuid return self._perform_generic_form_login( username=username, password=password, form_selector_pattern=r'
str: - """Get client ID from RTL+ website configuration using base class method""" if self._client_id: return self._client_id - # Use base class method for extraction self._client_id = self._extract_client_id_from_js( main_page_url=self.config.base_website, js_file_pattern=r'