Fix get_manifest

This commit is contained in:
Nirvana
2025-11-17 19:41:03 +01:00
parent a6e28881c0
commit 2814e9f1be
3 changed files with 167 additions and 57 deletions
@@ -327,7 +327,8 @@ class Magenta2Authenticator(BaseOAuth2Authenticator):
device_model: Optional[str] = None,
sam3_client_id: Optional[str] = None,
session_id: Optional[str] = None,
device_id: Optional[str] = None):
device_id: Optional[str] = None,
provider_config: Optional[Any] = None):
"""
Enhanced authenticator with complete authentication flow support
"""
@@ -355,6 +356,7 @@ class Magenta2Authenticator(BaseOAuth2Authenticator):
# Session and device management
self._session_id = session_id or str(uuid.uuid4())
self._device_id = device_id or str(uuid.uuid4())
self.provider_config = provider_config
# NEW: Store MPX account info for persona token composition
self._mpx_account_pid: Optional[str] = None
@@ -512,7 +514,8 @@ class Magenta2Authenticator(BaseOAuth2Authenticator):
sam3_client=self._sam3_client,
taa_client=self._taa_client,
provider_name=self.provider_name,
country=self.country
country=self.country,
provider_config=self.provider_config
)
logger.debug("TokenFlowManager initialized")
@@ -138,7 +138,8 @@ class Magenta2Provider(StreamingProvider):
device_model=f"{platform.upper()}_FTV", # Use fallback initially
sam3_client_id=fallback_client_id, # Use fallback initially
session_id=self.session_id,
device_id=self.device_id
device_id=self.device_id,
provider_config=None
)
# 🚨 NOW PERFORM CONFIGURATION DISCOVERY (authenticator exists)
@@ -151,6 +152,8 @@ class Magenta2Provider(StreamingProvider):
# 🚨 UPDATE AUTHENTICATOR WITH DISCOVERED CONFIG
if self.provider_config:
# Update authenticator with discovered client_id and models
self.authenticator.provider_config = self.provider_config # ✅ Store the config
logger.info("✓ ProviderConfig stored in authenticator")
if self.provider_config.bootstrap.sam3_client_id:
# Use public method if available, otherwise update directly
if hasattr(self.authenticator, 'update_sam3_client_id'):
@@ -355,10 +358,9 @@ class Magenta2Provider(StreamingProvider):
'x-dt-call-id': self._generate_call_id()
}
def _get_api_headers(self, use_persona_token: bool = False,
require_auth: bool = False) -> Dict[str, str]:
def _get_api_headers(self, require_auth: bool = False) -> Dict[str, str]:
"""
FIXED: Get headers for API requests with proper token usage
Get headers for API requests with persona_token Basic auth
"""
headers = {
'User-Agent': self.platform_config['user_agent'],
@@ -367,19 +369,20 @@ class Magenta2Provider(StreamingProvider):
}
# Lazy authentication - only authenticate if required and not already done
if require_auth and not self.bearer_token:
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}")
# CRITICAL FIX: Use COMPOSED persona token for API calls when requested
if use_persona_token and self.persona_token:
# 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 composed persona token (Basic auth)")
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")
logger.debug("Using TAA bearer token (fallback)")
return headers
@@ -475,18 +478,15 @@ class Magenta2Provider(StreamingProvider):
logger.error(f"Device registration failed: {e}")
return False
def get_yo_digital_token(self, force_refresh: bool = False) -> str:
"""Get yo_digital access token following complete hierarchy"""
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
# Try TokenFlowManager via public API
yo_digital_token = self.authenticator.get_yo_digital_token(force_refresh)
if yo_digital_token:
logger.info("✓ Got yo_digital token")
return yo_digital_token
# Fallback to existing authentication
logger.info("Using legacy authentication flow")
# Fallback to legacy authentication if persona token fails
logger.info("Using legacy authentication flow as fallback")
return self.authenticate(force_refresh=force_refresh)
def authenticate(self, **kwargs) -> str:
@@ -545,32 +545,18 @@ class Magenta2Provider(StreamingProvider):
return self.bearer_token
def _ensure_authenticated(self) -> None:
"""Ensure we have valid authentication token"""
# Try TokenFlowManager first (gets yo_digital)
if hasattr(self.authenticator, 'get_yo_digital_token'):
yo_digital_token = self.authenticator.get_yo_digital_token()
if yo_digital_token:
self.bearer_token = yo_digital_token
logger.info("✓ Lazy authentication via yo_digital")
"""Ensure we have valid persona token"""
if hasattr(self.authenticator, 'get_persona_token'):
persona_token = self.authenticator.get_persona_token()
if persona_token:
self.persona_token = persona_token
logger.info("✓ Lazy authentication via persona token")
return
# Fallback to old flow
# Fallback to old flow (for backward compatibility)
logger.info("Using legacy authentication for lazy auth")
self.bearer_token = self.authenticator.get_bearer_token()
# CRITICAL: Get composed persona token
self.persona_token = self.authenticator.get_persona_token()
if not self.persona_token:
# Debug information
auth_state = self.authenticator.debug_authentication_state()
logger.error(f"Authentication state: {json.dumps(auth_state, indent=2)}")
raise Exception(
"Lazy authentication succeeded but persona token composition failed. "
"Check if TAA JWT contains dc_cts_persona_token and account_uri claims."
)
logger.info("✓ Lazy authentication complete with persona token")
# Note: Legacy flow might not set persona_token
def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
return None
@@ -806,13 +792,13 @@ class Magenta2Provider(StreamingProvider):
def get_entitlement_token(self, content_id: str, content_type: str = CONTENT_TYPE_LIVE) -> str:
"""
FIXED: Get entitlement token using COMPOSED persona token
Get entitlement token using persona_token Basic auth
"""
# Ensure we're authenticated with persona token
self._ensure_authenticated()
# CRITICAL: Use persona token (Basic auth) for entitlement
headers = self._get_api_headers(use_persona_token=True, require_auth=True)
# Use persona token (Basic auth) for entitlement
headers = self._get_api_headers(require_auth=True)
payload = {
"content_id": content_id,
@@ -1064,7 +1050,7 @@ class Magenta2Provider(StreamingProvider):
# Get required components for SMIL request
selector_service = self.endpoint_manager.get_endpoint('mpx_selector')
if not selector_service:
logger.error(f"No mpx_basic_url_selector_service endpoint found for channel {channel_id}")
logger.error(f"No mpx_selector endpoint found for channel {channel_id}")
return None
account_pid = self.provider_config.manifest.mpx.account_pid
@@ -1073,20 +1059,18 @@ class Magenta2Provider(StreamingProvider):
return None
# Get composed persona token for Basic auth
persona_token = self.authenticator.get_persona_token()
persona_token = self.get_persona_token()
if not persona_token:
logger.error(f"No composed persona token available for channel {channel_id}")
logger.error(f"No persona token available for channel {channel_id}")
return None
# Fixed client ID (can be parameterized later if needed)
# Fixed client ID
client_id = "a8198f31-b406-4177-8dee-f6216c356c75"
# Construct SMIL URL
smil_url = f"{selector_service}{account_pid}/media/{channel_id}?format=smil&formats=MPEG-DASH&tracking=true&clientId={client_id}"
logger.info(f"Requesting SMIL manifest for channel {channel_id}")
logger.debug(f"SMIL URL: {smil_url}")
logger.debug(f"Using account PID: {account_pid}, client ID: {client_id}")
# Make SMIL request with Basic auth
headers = {
@@ -1103,14 +1087,13 @@ class Magenta2Provider(StreamingProvider):
)
response.raise_for_status()
logger.info(f"✓ SMIL response received for channel {channel_id} (status: {response.status_code})")
logger.info(f"✓ SMIL response received for channel {channel_id}")
# Parse SMIL to extract MPD URL
mpd_url = self._parse_smil_for_mpd(response.text, channel_id)
if mpd_url:
logger.info(f"✓ MPD manifest URL extracted for channel {channel_id}")
logger.debug(f"MPD URL: {mpd_url}")
else:
logger.warning(f"✗ No MPD URL found in SMIL response for channel {channel_id}")
@@ -6,6 +6,8 @@ yo_digital → taa → tvhubs → line_auth/remote_login
"""
import time
import base64
import json
from typing import Optional, Dict, Any
from dataclasses import dataclass
@@ -26,6 +28,12 @@ class TokenFlowResult:
error: Optional[str] = None
flow_path: Optional[str] = None # For debugging which path was taken
@dataclass
class PersonaResult:
success: bool
persona_token: Optional[str] = None
error: Optional[str] = None
class TokenFlowManager:
"""
@@ -51,7 +59,8 @@ class TokenFlowManager:
sam3_client: 'Sam3Client',
taa_client: 'TaaClient',
provider_name: str,
country: Optional[str] = None):
country: Optional[str] = None,
provider_config: Optional[Any] = None):
"""
Initialize TokenFlowManager
@@ -67,10 +76,125 @@ class TokenFlowManager:
self.taa_client = taa_client
self.provider_name = provider_name
self.country = country
self.provider_config = provider_config
logger.debug(f"TokenFlowManager initialized for {provider_name}" +
(f" ({country})" if country else ""))
@staticmethod
def _extract_jwt_claims(jwt_token: str) -> Dict[str, Any]:
"""Extract claims from JWT token"""
try:
parts = jwt_token.split('.')
if len(parts) != 3:
logger.warning("Invalid JWT format")
return {}
# Decode payload
payload_b64 = parts[1]
padding = len(payload_b64) % 4
if padding:
payload_b64 += '=' * (4 - padding)
payload_json = base64.b64decode(payload_b64).decode('utf-8')
claims = json.loads(payload_json)
logger.debug(f"JWT claims extracted: {list(claims.keys())}")
return claims
except Exception as e:
logger.error(f"Failed to extract JWT claims: {e}")
return {}
def _compose_persona_token(self, access_token: str) -> Optional[str]:
"""
Compose persona token from access_token JWT claims
"""
try:
# Import here to avoid circular imports
from .config_models import ProviderConfig
# Extract dc_cts_persona_token from JWT
claims = self._extract_jwt_claims(access_token)
dc_cts_persona_token = claims.get('dc_cts_persona_token')
if not dc_cts_persona_token:
logger.error("No dc_cts_persona_token found in JWT claims")
return None
# Use the discovered account URI from ProviderConfig
if not hasattr(self, 'provider_config') or not self.provider_config:
logger.error("No provider_config available - cannot compose persona token")
return None
# Type check to ensure it's a ProviderConfig
if not isinstance(self.provider_config, ProviderConfig):
logger.error(f"provider_config is not ProviderConfig: {type(self.provider_config)}")
return None
# CORRECTED: Use get_mpx_account_uri() method from ProviderConfig
account_uri = self.provider_config.get_mpx_account_uri()
if not account_uri:
logger.error("No account URI available from provider_config")
return None
logger.debug(f"Using discovered account URI: {account_uri}")
# Compose: account_uri + ":" + dc_cts_persona_token
raw_token = f"{account_uri}:{dc_cts_persona_token}"
# Base64 encode
persona_token = base64.b64encode(raw_token.encode('utf-8')).decode('utf-8')
logger.info("✓ Persona token composed successfully")
logger.debug(f"Account URI: {account_uri}")
logger.debug(f"Persona token preview: {persona_token[:50]}...")
return persona_token
except ImportError as e:
logger.error(f"Failed to import ProviderConfig: {e}")
return None
except Exception as e:
logger.error(f"Failed to compose persona token: {e}")
return None
def get_persona_token(self, force_refresh: bool = False) -> PersonaResult:
"""
Get persona token - compose from successful yo_digital token
"""
# Get access_token using existing method
token_result = self.get_yo_digital_token(force_refresh)
# If it failed, return the failure
if not token_result.success:
return PersonaResult(
success=False,
error=token_result.error,
)
# We have success - extract access_token and compose persona_token
access_token = token_result.access_token
if not access_token:
return PersonaResult(
success=False,
error="Failed to get yo_digital access token"
)
# Compose persona_token
persona_token = self._compose_persona_token(access_token)
if not persona_token:
return PersonaResult(
success=False,
error="Failed to compose persona token"
)
return PersonaResult(
success=True,
persona_token=persona_token
)
def get_yo_digital_token(self, force_refresh: bool = False) -> TokenFlowResult:
"""
Get yo_digital access token following the complete hierarchy