Fix taa_client

This commit is contained in:
Nirvana
2025-11-15 15:22:19 +01:00
parent ee0e09c3a1
commit 727bcff258
2 changed files with 308 additions and 11 deletions
@@ -234,7 +234,7 @@ class SessionManager:
def load_scoped_token(self, provider: str, scope: str,
country: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Load token data for a specific scope
Load token data for a specific scope (ENHANCED VERSION)
Args:
provider: Provider name
@@ -243,6 +243,11 @@ class SessionManager:
Returns:
Token data dictionary or None
Enhanced:
- Returns token even if access_token expired (for refresh attempts)
- Properly handles yo_digital tokens with separate expiry times
- Logs detailed expiration status
"""
country_str = f" (country: {country})" if country else ""
@@ -266,10 +271,28 @@ class SessionManager:
logger.warning(f"Scope '{scope}' exists but doesn't contain valid token data")
return None
# Check if token is expired
if self._is_token_expired(token_data):
# Check access token expiration
access_token_expired = self._is_token_expired(token_data)
# For yo_digital, also check refresh token
if scope == 'yo_digital' and 'refresh_token' in token_data:
refresh_token_expired = self._is_refresh_token_expired(token_data)
if access_token_expired and not refresh_token_expired:
logger.info(f"Access token expired for scope '{scope}' but refresh token still valid")
return token_data # Return so caller can attempt refresh
elif access_token_expired and refresh_token_expired:
logger.warning(f"Both access and refresh tokens expired for scope '{scope}'")
return None # Both expired, no point returning
else:
logger.info(f"Loaded valid token for {provider}{country_str}/{scope}")
return token_data
# Standard token handling (tvhubs, taa, etc.)
if access_token_expired:
logger.info(f"Token for scope '{scope}' is expired")
return token_data # Return anyway so refresh can be attempted
# Still return it - caller might want to attempt refresh or re-auth
return token_data
logger.info(f"Loaded valid token for {provider}{country_str}/{scope}")
return token_data
@@ -279,22 +302,103 @@ class SessionManager:
"""
Check if token is expired with buffer
Enhanced to support multiple expiration formats:
1. Standard format: expires_in + issued_at (for access_token)
2. yo_digital format: separate expiry for access_token and refresh_token
Args:
token_data: Token data dictionary
buffer_seconds: Seconds buffer before expiry (default 5 minutes)
Returns:
True if expired, False otherwise
Note:
For yo_digital tokens with both access_token and refresh_token,
this checks the access_token expiration only (not refresh_token).
"""
if 'expires_in' not in token_data or 'issued_at' not in token_data:
return False # Can't determine, assume valid
expires_in = token_data.get('expires_in', 0)
issued_at = token_data.get('issued_at', 0)
current_time = time.time()
expires_at = issued_at + expires_in
return current_time >= (expires_at - buffer_seconds)
# Format 1: Standard single token expiration
# Used by: tvhubs tokens, taa tokens, SAM3 tokens
if 'expires_in' in token_data and 'issued_at' in token_data:
expires_in = token_data.get('expires_in', 0)
issued_at = token_data.get('issued_at', 0)
expires_at = issued_at + expires_in
is_expired = current_time >= (expires_at - buffer_seconds)
if is_expired:
logger.debug(f"Token expired (standard format): "
f"issued_at={issued_at}, expires_in={expires_in}, "
f"expires_at={expires_at}, current={current_time}")
return is_expired
# Format 2: yo_digital separate expiration for access_token
# yo_digital tokens have separate expiry for access and refresh tokens
if 'access_token_expires_in' in token_data and 'access_token_issued_at' in token_data:
expires_in = token_data.get('access_token_expires_in', 0)
issued_at = token_data.get('access_token_issued_at', 0)
expires_at = issued_at + expires_in
is_expired = current_time >= (expires_at - buffer_seconds)
if is_expired:
logger.debug(f"Access token expired (yo_digital format): "
f"issued_at={issued_at}, expires_in={expires_in}, "
f"expires_at={expires_at}, current={current_time}")
return is_expired
# Format 3: Direct expiration timestamp (if some API returns 'exp' or 'expires_at')
if 'expires_at' in token_data:
expires_at = token_data.get('expires_at', 0)
is_expired = current_time >= (expires_at - buffer_seconds)
if is_expired:
logger.debug(f"Token expired (direct timestamp): "
f"expires_at={expires_at}, current={current_time}")
return is_expired
# No expiration info available - assume valid
logger.debug("No expiration info in token data - assuming valid")
return False
@staticmethod
def _is_refresh_token_expired(token_data: Dict[str, Any], buffer_seconds: int = 300) -> bool:
"""
Check if refresh token is expired (for yo_digital tokens)
This is a separate check specifically for yo_digital refresh tokens
which have their own expiration separate from access tokens.
Args:
token_data: Token data dictionary
buffer_seconds: Seconds buffer before expiry (default 5 minutes)
Returns:
True if refresh token expired, False otherwise or if no refresh token
"""
# yo_digital format with separate refresh token expiry
if 'refresh_token_expires_in' in token_data and 'refresh_token_issued_at' in token_data:
current_time = time.time()
expires_in = token_data.get('refresh_token_expires_in', 0)
issued_at = token_data.get('refresh_token_issued_at', 0)
expires_at = issued_at + expires_in
is_expired = current_time >= (expires_at - buffer_seconds)
if is_expired:
logger.debug(f"Refresh token expired (yo_digital format): "
f"issued_at={issued_at}, expires_in={expires_in}, "
f"expires_at={expires_at}, current={current_time}")
return is_expired
# No refresh token or no expiration info
return False
def save_token(self, provider: str, token: BaseAuthToken,
country: Optional[str] = None) -> bool:
@@ -547,6 +547,16 @@ class SettingsManager:
all_countries = self.session_manager.get_all_countries(provider_name)
status['configured_countries'] = all_countries
scoped_tokens = {}
available_scopes = self.list_scoped_tokens(provider_name, country)
for scope in available_scopes:
scope_status = self.get_scoped_token_status(provider_name, scope, country)
scoped_tokens[scope] = scope_status
if scoped_tokens:
status['scoped_tokens'] = scoped_tokens
return status
def is_provider_ready(self, provider_name: str, country: Optional[str] = None) -> bool:
@@ -1122,6 +1132,189 @@ class SettingsManager:
logger.error(f"Error migrating {provider_name} to country structure: {e}")
return False
# ============= Scoped Token Management (Pass-through to SessionManager) =============
def save_scoped_token(self, provider_name: str, scope: str, token_data: Dict[str, Any],
country: Optional[str] = None) -> bool:
"""
Save authentication token for a specific scope
Pass-through method to SessionManager.save_scoped_token()
Args:
provider_name: Name of the provider
scope: Token scope (e.g., 'tvhubs', 'taa', 'yo_digital')
token_data: Token data to save (should include access_token, expires_in, etc.)
country: Optional country code
Returns:
True if successful, False otherwise
Examples:
# Save tvhubs token (access token only)
settings_manager.save_scoped_token('magenta2', 'tvhubs', {
'access_token': '...',
'token_type': 'Bearer',
'expires_in': 7200,
'issued_at': 1234567890
}, 'de')
# Save yo_digital token (access + refresh, each with own expiry)
settings_manager.save_scoped_token('magenta2', 'yo_digital', {
'access_token': '...',
'access_token_expires_in': 3600,
'access_token_issued_at': 1234567890,
'refresh_token': '...',
'refresh_token_expires_in': 86400,
'refresh_token_issued_at': 1234567890,
'token_type': 'Bearer'
}, 'de')
"""
return self.session_manager.save_scoped_token(provider_name, scope, token_data, country)
def load_scoped_token(self, provider_name: str, scope: str,
country: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Load token data for a specific scope
Pass-through method to SessionManager.load_scoped_token()
Args:
provider_name: Name of the provider
scope: Token scope (e.g., 'tvhubs', 'taa', 'yo_digital')
country: Optional country code
Returns:
Token data dictionary or None if not found or expired
Note:
This method checks token expiration automatically.
For yo_digital tokens, it checks both access_token and refresh_token expiry.
"""
return self.session_manager.load_scoped_token(provider_name, scope, country)
def clear_scoped_token(self, provider_name: str, scope: str,
country: Optional[str] = None) -> bool:
"""
Clear token for a specific scope
Pass-through method to SessionManager.clear_scoped_token()
Args:
provider_name: Name of the provider
scope: Token scope to clear
country: Optional country code
Returns:
True if successful, False otherwise
Example:
# Clear only the tvhubs token, keep taa and yo_digital
settings_manager.clear_scoped_token('magenta2', 'tvhubs', 'de')
"""
return self.session_manager.clear_scoped_token(provider_name, scope, country)
def list_scoped_tokens(self, provider_name: str, country: Optional[str] = None) -> List[str]:
"""
List all available token scopes for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
List of scope names (e.g., ['tvhubs', 'taa', 'yo_digital'])
Example:
scopes = settings_manager.list_scoped_tokens('magenta2', 'de')
# Returns: ['tvhubs', 'taa', 'yo_digital']
"""
session_data = self.session_manager.load_session(provider_name, country)
if not session_data:
return []
# Filter out non-scope keys (device_id, refresh_token at provider level)
non_scope_keys = {'device_id', 'refresh_token'}
scopes = [
key for key in session_data.keys()
if isinstance(session_data[key], dict) and key not in non_scope_keys
]
return scopes
def get_scoped_token_status(self, provider_name: str, scope: str,
country: Optional[str] = None) -> Dict[str, Any]:
"""
Get detailed status information for a scoped token
Args:
provider_name: Name of the provider
scope: Token scope
country: Optional country code
Returns:
Dictionary with token status information
Example:
status = settings_manager.get_scoped_token_status('magenta2', 'yo_digital', 'de')
# Returns:
# {
# 'exists': True,
# 'access_token_valid': True,
# 'access_token_expires_at': 1234567890,
# 'refresh_token_valid': True,
# 'refresh_token_expires_at': 1234654290,
# 'scope': 'yo_digital'
# }
"""
token_data = self.load_scoped_token(provider_name, scope, country)
if not token_data:
return {
'exists': False,
'scope': scope,
'provider_name': provider_name,
'country': country
}
status = {
'exists': True,
'scope': scope,
'provider_name': provider_name,
'country': country,
'token_type': token_data.get('token_type', 'Bearer')
}
# Check access token expiration
if 'access_token' in token_data:
status['has_access_token'] = True
# Standard expiration (single expires_in and issued_at)
if 'expires_in' in token_data and 'issued_at' in token_data:
expires_at = token_data['issued_at'] + token_data['expires_in']
status['access_token_expires_at'] = expires_at
status['access_token_valid'] = time.time() < expires_at
# yo_digital style (separate access_token_expires_in)
elif 'access_token_expires_in' in token_data and 'access_token_issued_at' in token_data:
expires_at = token_data['access_token_issued_at'] + token_data['access_token_expires_in']
status['access_token_expires_at'] = expires_at
status['access_token_valid'] = time.time() < expires_at
else:
status['access_token_valid'] = None # Cannot determine
# Check refresh token expiration (for yo_digital)
if 'refresh_token' in token_data:
status['has_refresh_token'] = True
if 'refresh_token_expires_in' in token_data and 'refresh_token_issued_at' in token_data:
expires_at = token_data['refresh_token_issued_at'] + token_data['refresh_token_expires_in']
status['refresh_token_expires_at'] = expires_at
status['refresh_token_valid'] = time.time() < expires_at
else:
status['refresh_token_valid'] = None # Cannot determine
return status
# For imports that expect the old interface
UnifiedSettingsManager = SettingsManager # Backward compatibility alias