From a9ac7acb89efbd56859a2147aefaad171c447f7d Mon Sep 17 00:00:00 2001 From: Nirvana Date: Thu, 20 Nov 2025 12:30:13 +0100 Subject: [PATCH] Fix get_manifest --- .../providers/magenta2/auth.py | 154 +--------- .../providers/magenta2/concurrency.py | 24 +- .../providers/magenta2/provider.py | 274 +++++++----------- 3 files changed, 138 insertions(+), 314 deletions(-) diff --git a/lib/streaming_providers/providers/magenta2/auth.py b/lib/streaming_providers/providers/magenta2/auth.py index 7215d61..d3f97e9 100644 --- a/lib/streaming_providers/providers/magenta2/auth.py +++ b/lib/streaming_providers/providers/magenta2/auth.py @@ -834,26 +834,6 @@ class Magenta2Authenticator(BaseOAuth2Authenticator): country=self.country ) - def _perform_authentication(self) -> BaseAuthToken: - """ - ENHANCED: Perform Magenta2 authentication with line auth priority - """ - # NEW: Try line auth first if we have the required components - if self._should_try_line_auth_first(): - logger.info("Attempting line authentication first (device token available)") - try: - return self._perform_line_auth_flow() - except Exception as e: - logger.warning(f"Line auth failed, falling back to standard flow: {e}") - - # Continue with existing logic - if isinstance(self.credentials, Magenta2UserCredentials) and self.credentials.has_user_credentials(): - logger.info(f"Using complete user authentication flow for {self.provider_name}") - return self._perform_user_authentication_flow() - else: - logger.info(f"Using client credentials TAA flow for {self.provider_name}") - return self._perform_taa_flow() - def _should_try_line_auth_first(self) -> bool: """Check if we should attempt line auth first""" return ( @@ -862,46 +842,6 @@ class Magenta2Authenticator(BaseOAuth2Authenticator): self._sam3_client is not None ) - def _perform_line_auth_flow(self) -> BaseAuthToken: - """ - Complete authentication flow starting with line auth with automatic remote login fallback - """ - logger.debug("Starting line authentication flow with remote login fallback") - - # Step 1: Try line authentication first - try: - line_response_data = self._perform_line_auth_with_response() - if line_response_data: - logger.info("✓ Line auth succeeded") - return self._process_line_auth_success(line_response_data) - except Exception as e: - logger.warning(f"Line auth failed: {e}") - - # Step 2: Line auth failed, try remote login fallback - logger.info("Line auth failed, attempting remote login fallback") - - if not self._sam3_client or not self._sam3_client.can_use_remote_login(): - logger.error("Remote login not available as fallback") - raise Exception("Line auth failed and remote login not available") - - try: - # Perform remote login (notifier handled internally) - remote_token_data = self._sam3_client.remote_login( - scope="tvhubs offline_access" - ) - - if not remote_token_data: - raise Exception("Remote login failed or timed out") - - logger.info("✓ Remote login fallback successful") - - # Process remote login token same as line auth - return self._process_line_auth_success(remote_token_data) - - except Exception as e: - logger.error(f"Remote login fallback failed: {e}") - raise Exception(f"Both line auth and remote login failed: {e}") - def _process_line_auth_success(self, line_response_data: Dict[str, Any]) -> BaseAuthToken: """ Process successful line auth or remote login response @@ -1182,86 +1122,6 @@ class Magenta2Authenticator(BaseOAuth2Authenticator): return self.credentials.username, self.credentials.password - def _perform_user_authentication_flow(self) -> BaseAuthToken: - """ - COMPLETE USER FLOW: SAM3 → SSO → TAA - """ - try: - logger.debug("Starting complete Magenta2 user authentication flow") - - # Step 1: SAM3 login with username/password - if not self._sam3_client: - raise Exception("SAM3 client not initialized") - - # FIXED: Use safe credential access - username, password = self._get_user_credentials() - - sam3_result = self._sam3_client.sam3_login(username, password) - - # Step 2: SSO authentication with code/state - if not self._sso_client: - raise Exception("SSO client not initialized") - - sso_result = self._sso_client.sso_authenticate( - code=sam3_result['code'], - state=sam3_result['state'] - ) - - # Step 3: Get SAM3 access token for TAA scope - sam3_taa_token = self._get_sam3_token_for_taa() - - # Step 4: Perform TAA authentication with SAM3 token - taa_result = self._perform_taa_authentication(sam3_taa_token) - - # Step 5: Create final token with combined data - token = self._create_token_from_combined_data(sso_result, taa_result) - - logger.info("✓ Complete user authentication flow successful") - return token - - except Exception as e: - logger.error(f"User authentication flow failed: {e}") - # Fall back to TAA-only flow if user auth fails - logger.warning("Falling back to TAA-only authentication") - return self._perform_taa_flow() - - def _perform_taa_flow(self) -> BaseAuthToken: - """ - Perform TAA authentication flow using dedicated TaaClient - """ - try: - logger.debug("Starting Magenta2 TAA authentication flow") - - # Step 1: Get SAM3 access token for TAA - sam3_token = self._get_sam3_token() - if not sam3_token: - raise Exception("Could not obtain SAM3 token for TAA") - - # Step 2: Perform TAA authentication using TaaClient - if not self._taa_client: - raise Exception("TAA client not initialized") - - taa_result = self._taa_client.authenticate( - sam3_token=sam3_token, - device_id=self._device_id, - client_model=self._client_model, - device_model=None, - taa_endpoint=self._get_endpoint('taa_auth', 'TAA_AUTH') - ) - - # Check for device limit - if taa_result.device_limit_exceeded: - from .models import DeviceLimitExceededException - raise DeviceLimitExceededException("Device limit exceeded for Magenta2") - - # Step 3: Convert TaaAuthResult to BaseAuthToken - token = self._create_token_from_taa_result(taa_result) - return token - - except Exception as e: - logger.error(f"TAA authentication flow failed: {e}") - raise Exception(f"TAA authentication failed: {e}") - def _create_token_from_taa_result(self, taa_result: TaaAuthResult) -> BaseAuthToken: """ Create authentication token from TaaAuthResult @@ -1620,15 +1480,23 @@ class Magenta2Authenticator(BaseOAuth2Authenticator): } } - # Required abstract method from BaseOAuth2Authenticator def _perform_oauth_authorization_code_flow(self, username: str, password: str) -> Dict[str, Any]: """ OAuth2 authorization code flow - now implemented via SAM3 + SSO """ try: # Use our complete user authentication flow - token = self._perform_user_authentication_flow() - return token.to_dict() + persona_token = self.get_persona_token() + if not persona_token: + raise Exception("Failed to get persona token") + + # Return in the expected OAuth2 response format + return { + 'access_token': persona_token, + 'token_type': 'Basic', + 'expires_in': 3600, # Default expiration + 'issued_at': time.time() + } except Exception as e: logger.error(f"OAuth2 authorization code flow failed: {e}") raise diff --git a/lib/streaming_providers/providers/magenta2/concurrency.py b/lib/streaming_providers/providers/magenta2/concurrency.py index 9021eb6..a50b9a6 100644 --- a/lib/streaming_providers/providers/magenta2/concurrency.py +++ b/lib/streaming_providers/providers/magenta2/concurrency.py @@ -42,7 +42,7 @@ def extract_and_release_lock(smil_content: str, http_manager: HTTPManager, return False # Build release URL with the same client_id formatted as player_{client_id} - base_url = lock_params['concurrencyServiceUrl'].rstrip('/') + "/web/Concurrency/release" + base_url = lock_params['concurrencyServiceUrl'].rstrip('/') + "/web/Concurrency/unlock" formatted_client_id = f"player_{client_id}" params = { @@ -64,19 +64,33 @@ def extract_and_release_lock(smil_content: str, http_manager: HTTPManager, 'Accept': 'application/json' } + # Release the lock immediately # Release the lock immediately response = http_manager.get( release_url, - operation='concurrency_release', + operation='concurrency_unlock', headers=headers, timeout=10 ) + # Log the response for debugging + logger.debug(f"Concurrency unlock response: Status={response.status_code}, Content={response.text}") + if response.status_code == 200: - logger.info(f"✓ Concurrency lock released successfully with client: {formatted_client_id}") - return True + try: + # Parse JSON response to verify it contains unlockResponse + response_data = response.json() + if 'unlockResponse' in response_data: + logger.info(f"✓ Concurrency lock released successfully with client: {formatted_client_id}") + return True + else: + logger.warning(f"Concurrency lock release failed - missing unlockResponse: {response_data}") + return False + except Exception as e: + logger.warning(f"Concurrency lock release - invalid JSON response: {e}, Content: {response.text}") + return False else: - logger.warning(f"Concurrency lock release failed: {response.status_code}") + logger.warning(f"Concurrency lock release failed with status: {response.status_code}") return False except Exception as e: diff --git a/lib/streaming_providers/providers/magenta2/provider.py b/lib/streaming_providers/providers/magenta2/provider.py index 72f2a52..548c15b 100644 --- a/lib/streaming_providers/providers/magenta2/provider.py +++ b/lib/streaming_providers/providers/magenta2/provider.py @@ -233,8 +233,6 @@ class Magenta2Provider(StreamingProvider): logger.warning("✗ Failed to update SAM3 client with QR code URL") # Initialize auth tokens (lazy - populated on first use) - self.bearer_token = None # TAA access token (JWT) - self.persona_token = None # COMPOSED persona token (Base64) for API calls self.device_token = None logger.info("Magenta2 provider initialization completed successfully") @@ -366,30 +364,16 @@ class Magenta2Provider(StreamingProvider): } def _get_api_headers(self, require_auth: bool = False) -> Dict[str, str]: - """ - Get headers for API requests with persona_token Basic auth - """ + """Get headers for API requests""" headers = { 'User-Agent': self.platform_config['user_agent'], 'Accept': 'application/json', 'Content-Type': 'application/json' } - # Lazy authentication - only authenticate if required and not already done - if require_auth and not self.persona_token: - try: - self._ensure_authenticated() - except Exception as e: - logger.warning(f"Could not authenticate for API headers: {e}") - - # Use persona token for Basic auth when available - if require_auth and self.persona_token: - headers['Authorization'] = f'Basic {self.persona_token}' - logger.debug("Using persona token (Basic auth) for API call") - elif self.bearer_token: - # Fallback to Bearer token for backward compatibility - headers['Authorization'] = f'Bearer {self.bearer_token}' - logger.debug("Using TAA bearer token (fallback)") + if require_auth: + persona_token = self._ensure_authenticated() + headers['Authorization'] = f'Basic {persona_token}' return headers @@ -486,100 +470,27 @@ class Magenta2Provider(StreamingProvider): return False def get_persona_token(self, force_refresh: bool = False) -> str: - """Get persona token for ALL API calls""" - persona_token = self.authenticator.get_persona_token() - if persona_token: - logger.info("✓ Got persona token") - return persona_token + """ + Get persona token - ONLY authentication entry point - # Fallback to legacy authentication if persona token fails - logger.info("Using legacy authentication flow as fallback") - return self.authenticate(force_refresh=force_refresh) + Raises: + Exception: If persona token cannot be obtained + """ + if not self.authenticator.token_flow_manager: + raise Exception("TokenFlowManager not initialized") - def authenticate(self, **kwargs) -> str: - """Authenticate and get access token""" - force_refresh = kwargs.get('force_refresh', False) - - # Try new yo_digital flow first (unless force_legacy is set) - if not kwargs.get('force_legacy', False): - yo_digital_token = self.authenticator.get_yo_digital_token(force_refresh) - - if yo_digital_token: - self.bearer_token = yo_digital_token - logger.info("✓ Authentication via yo_digital") - return self.bearer_token - - # Fallback to existing line_auth + TAA flow - logger.info("Using legacy authentication flow") - - line_auth_available = ( - hasattr(self.authenticator, 'can_use_line_auth') and - self.authenticator.can_use_line_auth() + persona_result = self.authenticator.token_flow_manager.get_persona_token( + force_refresh=force_refresh ) - if line_auth_available and not kwargs.get('force_client_credentials', False): - logger.info("Line auth components available, attempting line auth flow") + if not persona_result.success: + raise Exception(f"Failed to get persona token: {persona_result.error}") - # Continue with existing logic (which should now try line auth first in auth.py) - self.bearer_token = self.authenticator.get_bearer_token( - force_refresh=kwargs.get('force_refresh', False) - ) + return persona_result.persona_token - # CRITICAL: Extract the COMPOSED persona token - self.persona_token = self.authenticator.get_persona_token() - - if not self.persona_token: - logger.error("Authentication succeeded but no composed persona token available!") - auth_state = self.authenticator.debug_authentication_state() - logger.error(f"Auth state: {json.dumps(auth_state, indent=2)}") - raise Exception( - "Failed to compose persona token. " - "TAA JWT may be missing dc_cts_persona_token or account_uri claims." - ) - - logger.info("✓ Authentication complete with composed persona token") - return self.bearer_token - - def refresh_authentication(self) -> str: - """FIXED: Force refresh authentication and get new persona token""" - self.bearer_token = self.authenticator.get_bearer_token(force_refresh=True) - self.persona_token = self.authenticator.get_persona_token() - - if not self.persona_token: - raise Exception("Failed to compose persona token after refresh") - - logger.info("✓ Authentication refreshed with new persona token") - return self.bearer_token - - def _ensure_authenticated(self) -> None: - """Ensure we have valid persona token for API calls""" - if self.persona_token: - return # Already have persona token - - # Get persona token via TokenFlowManager - if hasattr(self.authenticator, 'token_flow_manager') and self.authenticator.token_flow_manager: - persona_result = self.authenticator.token_flow_manager.get_persona_token() - if persona_result.success: - self.persona_token = persona_result.persona_token - logger.info("✓ Persona token obtained via TokenFlowManager") - return - - # Fallback: try to get persona token from authenticator - persona_token = self.authenticator.get_persona_token() - if persona_token: - self.persona_token = persona_token - logger.info("✓ Persona token obtained via authenticator") - return - - # Last resort: force authentication - logger.warning("No persona token available, forcing authentication") - self.authenticate(force_refresh=True) - persona_token = self.authenticator.get_persona_token() - if persona_token: - self.persona_token = persona_token - logger.info("✓ Persona token obtained after forced authentication") - else: - logger.error("✗ Failed to obtain persona token after forced authentication") + def _ensure_authenticated(self) -> str: + """Ensure we have a valid persona token""" + return self.get_persona_token(force_refresh=False) def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]: return None @@ -1239,9 +1150,9 @@ class Magenta2Provider(StreamingProvider): """Get SMIL content for a channel to extract releasePid and release concurrency lock""" try: # Reuse the same logic as get_manifest but return the raw SMIL content - self._ensure_authenticated() + persona_token = self._ensure_authenticated() - if not self.persona_token: + if not persona_token: return None selector_service = self.endpoint_manager.get_endpoint('mpx_selector') @@ -1256,8 +1167,10 @@ class Magenta2Provider(StreamingProvider): client_id = "a8198f31-b406-4177-8dee-f6216c356c75" smil_url = f"{selector_service}{account_pid}/media/{channel_id}?format=smil&formats=MPEG-DASH&tracking=true&clientId={client_id}" + persona_token = self._ensure_authenticated() + headers = { - 'Authorization': f'Basic {self.persona_token}', + 'Authorization': f'Basic {persona_token}', 'User-Agent': self.platform_config['user_agent'], # Platform user agent 'Accept': 'application/smil+xml, application/xml;q=0.9, */*;q=0.8' } @@ -1327,41 +1240,29 @@ class Magenta2Provider(StreamingProvider): logger.error(f"Error extracting releasePid from SMIL: {e}") return None - def _extract_persona_jwt_token(self) -> Optional[str]: - """Extract the raw persona JWT token from the composed persona token""" + @staticmethod + def _extract_persona_jwt_from_token(persona_token: str) -> Optional[str]: + """ + Extract the raw persona JWT token from Base64-encoded persona token + + The persona token format is: Base64(account_uri + ":" + persona_jwt) + This method decodes it and extracts just the persona_jwt part. + """ try: - if not self.persona_token: - return None - - # Decode the base64 persona token - decoded = base64.b64decode(self.persona_token).decode('utf-8') - logger.debug(f"Decoded persona token: {decoded[:100]}...") - - # The format is: account_uri:persona_jwt - # But account_uri contains colons (http://), so we need to split carefully - - # Find the last colon (after the account URI) + decoded = base64.b64decode(persona_token).decode('utf-8') last_colon_index = decoded.rfind(':') - if last_colon_index != -1: - account_uri = decoded[:last_colon_index] - persona_jwt = decoded[last_colon_index + 1:] - - logger.debug(f"Account URI: {account_uri}") - logger.debug(f"Extracted persona JWT token length: {len(persona_jwt)}") - logger.debug(f"Persona JWT token preview: {persona_jwt[:50]}...") - - # Verify it's a JWT token (should start with eyJ) - if persona_jwt.startswith('eyJ'): - return persona_jwt - else: - logger.error(f"Extracted token doesn't look like a JWT: {persona_jwt[:20]}...") - return None - else: + if last_colon_index == -1: logger.error("No colon found in decoded persona token") - logger.debug(f"Decoded token: {decoded}") return None + persona_jwt = decoded[last_colon_index + 1:] + + if not persona_jwt.startswith('eyJ'): + logger.error(f"Extracted token doesn't look like a JWT") + return None + + return persona_jwt except Exception as e: logger.error(f"Error extracting persona JWT token: {e}") return None @@ -1370,7 +1271,6 @@ class Magenta2Provider(StreamingProvider): **kwargs) -> List[DRMConfig]: """Get DRM configuration using unified SMIL data""" try: - # Get SMIL data with releasePid smil_data = self._get_smil_data(channel_id) if not smil_data or not smil_data['release_pid']: logger.error(f"No releasePid found in SMIL for channel {channel_id}") @@ -1378,30 +1278,33 @@ class Magenta2Provider(StreamingProvider): release_pid = smil_data['release_pid'] - # Rest of the DRM logic remains the same... - self._ensure_authenticated() - if not self.persona_token: + # Get persona token + persona_token = self._ensure_authenticated() + + # Extract the persona JWT from the Base64-encoded persona token + raw_persona_jwt = self._extract_persona_jwt_from_token(persona_token) + if not raw_persona_jwt: + logger.error("Failed to extract persona JWT from persona token") return [] - raw_persona_token = self._extract_persona_jwt_token() - if not raw_persona_token: - return [] - - # ... continue with existing DRM logic + # Get widevine endpoint widevine_endpoint = self.endpoint_manager.get_endpoint('widevine_license') if not widevine_endpoint: + logger.error("No widevine license endpoint available") return [] + # Get account URI account_uri = self._get_account_uri() encoded_account_uri = self._url_encode(account_uri) + # Build license URL with the extracted JWT license_url = (f"{widevine_endpoint}?" f"schema=1.0&" f"releasePid={release_pid}&" - f"token={raw_persona_token}&" + f"token={raw_persona_jwt}&" f"account={encoded_account_uri}") - # Create DRM config... + # Create DRM config drm_config = DRMConfig( system=DRMSystem.WIDEVINE, priority=1, @@ -1470,35 +1373,74 @@ class Magenta2Provider(StreamingProvider): def debug_authentication(self) -> Dict[str, Any]: """ - Enhanced debug method with authentication capabilities + Debug authentication state and token flow + + Returns comprehensive information about the current authentication state, + token availability, and TokenFlowManager status. """ - result = { + result: Dict[str, Any] = { 'provider': { - 'has_bearer_token': bool(self.bearer_token), - 'has_persona_token': bool(self.persona_token), - 'persona_token_preview': self.persona_token[:50] + '...' if self.persona_token else None, + 'provider_name': self.provider_name, + 'country': self.country, + 'platform': self.platform } } + # Try to get current persona token status + try: + persona_token = self.get_persona_token(force_refresh=False) + persona_info: Dict[str, Any] = { + 'available': True, + 'length': len(persona_token), + 'preview': persona_token[:50] + '...' + } + + # Try to extract and verify the JWT inside + try: + persona_jwt = self._extract_persona_jwt_from_token(persona_token) + persona_info['jwt_available'] = bool(persona_jwt) + if persona_jwt: + persona_info['jwt_length'] = len(persona_jwt) + persona_info['jwt_preview'] = persona_jwt[:50] + '...' + except Exception as e: + persona_info['jwt_extraction_error'] = str(e) + + result['persona_token'] = persona_info + + except Exception as e: + result['persona_token'] = { + 'available': False, + 'error': str(e) + } + + # TokenFlowManager status + if hasattr(self.authenticator, 'token_flow_manager') and self.authenticator.token_flow_manager: + result['token_flow_manager'] = { + 'available': True, + 'token_status': self.authenticator.token_flow_manager.get_token_status() + } + else: + result['token_flow_manager'] = { + 'available': False, + 'error': 'TokenFlowManager not initialized' + } + + # Authenticator capabilities if hasattr(self.authenticator, 'get_authentication_capabilities'): result['authentication_capabilities'] = self.authenticator.get_authentication_capabilities() - if hasattr(self.authenticator, 'debug_authentication_state'): - result['authenticator'] = self.authenticator.debug_authentication_state() - + # Endpoint manager info if self.endpoint_manager: result['endpoints'] = { 'has_taa_auth': self.endpoint_manager.has_endpoint('taa_auth'), 'has_entitlement': self.endpoint_manager.has_endpoint('entitlement'), + 'has_widevine_license': self.endpoint_manager.has_endpoint('widevine_license'), + 'has_mpx_selector': self.endpoint_manager.has_endpoint('mpx_selector'), 'total_endpoints': len(self.endpoint_manager.get_all_endpoints()), } - # PHASE 4: Add TAA token analysis if bearer token is available - if self.bearer_token and hasattr(self.authenticator, 'debug_taa_token'): - result['taa_token_analysis'] = self.authenticator.debug_taa_token(self.bearer_token) - - # PHASE 4: Add authentication flow info - if hasattr(self.authenticator, 'get_authentication_flow_info'): - result['authentication_flow'] = self.authenticator.get_authentication_flow_info() + # SAM3 client status + if hasattr(self.authenticator, 'get_sam3_client_status'): + result['sam3_client'] = self.authenticator.get_sam3_client_status() return result \ No newline at end of file