Files
script.service.ultimate/lib/streaming_providers/base/settings/settings_manager.py
T
2025-10-29 20:23:50 +01:00

1129 lines
46 KiB
Python

# streaming_providers/base/settings/settings_manager.py
"""
Unified Settings Manager - Country-Aware Implementation
Supports country-specific credentials, sessions, and configurations
"""
import json
import time
import os
from typing import Dict, Any, Optional, List
from pathlib import Path
from dataclasses import dataclass, field
from ..auth.session_manager import SessionManager
from ..auth.credential_manager import CredentialManager
from ..auth.credentials import BaseCredentials
from ..network.proxy_manager import ProxyConfigManager
from ..models.proxy_models import ProxyConfig
from ..utils.logger import logger
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .kodi_settings_bridge import KodiSettingsBridge
@dataclass
class ProviderRegistration:
"""Information about a registered provider"""
name: str
registered_at: float = field(default_factory=time.time)
is_active: bool = True
settings_schema: Optional[Dict[str, Any]] = None
supports_countries: bool = False # NEW: Flag if provider supports country-specific settings
available_countries: List[str] = field(default_factory=list) # NEW: List of supported countries
# Maintain backward compatibility by keeping the original class name
UnifiedSettingsManager = None # Will be set below for backward compatibility
class SettingsManager:
"""
Unified interface for all streaming provider settings management.
Now supports country-specific settings for multi-region providers.
"""
def __init__(self, config_dir: Optional[str] = None, enable_kodi_integration: bool = True):
"""
Initialize unified settings manager
Args:
config_dir: Optional config directory override
enable_kodi_integration: Enable Kodi settings bridge integration
"""
# Store config directory for component managers
self.config_dir_path = config_dir
# Initialize VFS for all file operations
from ..utils.vfs import VFS
self.vfs = VFS(config_dir=config_dir)
# Maintain config_path for backward compatibility
if config_dir:
self.config_path = Path(config_dir)
else:
# Let VFS handle default path resolution, but provide Path object if needed
self.config_path = Path(self.vfs.base_path) if self.vfs.base_path else None
self.settings_file = 'unified_settings.json'
# Initialize component managers - pass the original config_dir
self.session_manager = SessionManager(self.config_dir_path)
self.credential_manager = CredentialManager(self.config_dir_path)
self.proxy_manager = ProxyConfigManager(self.config_dir_path)
# Initialize Kodi integration if enabled
self.enable_kodi_integration = enable_kodi_integration
self.kodi_bridge: Optional['KodiSettingsBridge'] = None
if enable_kodi_integration:
success = self._setup_kodi_integration()
if not success:
self.kodi_bridge = None
self.enable_kodi_integration = False
# Provider registry
self._registered_providers: Dict[str, ProviderRegistration] = {}
# Load existing configuration
self._load_configuration()
# Perform Kodi sync if bridge is available and in Kodi environment
if self.enable_kodi_integration and self.kodi_bridge and self.kodi_bridge.is_kodi_environment():
logger.info("Performing initial sync from Kodi...")
# First, detect all providers from Kodi (including unregistered ones)
detected_providers = self.kodi_bridge.detect_all_providers_from_kodi()
logger.info(f"Detected providers from Kodi: {detected_providers}")
# Auto-register any unregistered providers
for provider_name, countries in detected_providers.items():
if not self.is_provider_registered(provider_name):
logger.info(f"Auto-registering provider '{provider_name}' from Kodi")
self.register_provider(
provider_name,
supports_countries=bool(countries),
available_countries=countries if countries else []
)
else:
# Provider already registered - check if we need to upgrade to multi-country
if countries and not self.provider_supports_countries(provider_name):
logger.info(f"Auto-upgrading existing provider '{provider_name}' to multi-country: {countries}")
reg = self._registered_providers[provider_name]
reg.supports_countries = True
reg.available_countries = countries
self._save_configuration()
# Now sync all registered providers
for provider_name in self.list_registered_providers():
# Check current registration status
if self.provider_supports_countries(provider_name):
# Multi-country provider - sync each country
available_countries = self.get_provider_countries(provider_name)
logger.info(f"Syncing multi-country provider {provider_name} for countries: {available_countries}")
for country in available_countries:
logger.info(f"Syncing credentials for {provider_name} ({country})...")
self._sync_credentials_from_kodi(provider_name, country)
logger.info(f"Syncing proxy for {provider_name} ({country})...")
self._sync_proxy_from_kodi(provider_name, country)
else:
# Single-country provider (backward compatibility)
logger.info(f"Syncing credentials for {provider_name}...")
self._sync_credentials_from_kodi(provider_name)
logger.info(f"Syncing proxy for {provider_name}...")
self._sync_proxy_from_kodi(provider_name)
logger.info(f"Initialized SettingsManager with config dir: {self.config_dir_path}")
def _setup_kodi_integration(self) -> bool:
"""Setup Kodi integration, return True if successful"""
try:
from .kodi_settings_bridge import KodiSettingsBridge
except ImportError:
logger.debug("KodiSettingsBridge not available")
return False
try:
self.kodi_bridge = KodiSettingsBridge()
if self.kodi_bridge and self.kodi_bridge.is_kodi_environment():
logger.info("Kodi integration enabled")
return True
else:
logger.debug("KodiSettingsBridge created but not in Kodi environment")
self.kodi_bridge = None
return False
except Exception as e:
logger.error(f"Failed to initialize Kodi bridge: {e}")
self.kodi_bridge = None
return False
# ============= Internal Methods =============
def _load_configuration(self) -> None:
"""Load settings manager configuration from file using VFS"""
if not self.vfs.exists(self.settings_file):
logger.debug("No unified settings file found, starting fresh")
return
try:
data = self.vfs.read_json(self.settings_file)
# Load provider registrations
providers_data = data.get('providers', {})
for provider_name, provider_info in providers_data.items():
try:
registration = ProviderRegistration(
name=provider_name,
registered_at=provider_info.get('registered_at', time.time()),
is_active=provider_info.get('is_active', True),
settings_schema=provider_info.get('settings_schema'),
supports_countries=provider_info.get('supports_countries', False),
available_countries=provider_info.get('available_countries', [])
)
self._registered_providers[provider_name] = registration
logger.debug(f"Loaded provider registration: {provider_name} "
f"(countries: {registration.supports_countries})")
except Exception as e:
logger.error(f"Error loading provider {provider_name}: {e}")
logger.info(f"Loaded {len(self._registered_providers)} provider registrations")
except Exception as e:
logger.error(f"Error loading unified settings configuration: {e}")
def _save_configuration(self) -> bool:
"""Save settings manager configuration to file using VFS"""
try:
data = {
'version': '2.1', # Bumped version for country support
'last_updated': time.time(),
'kodi_integration_enabled': self.enable_kodi_integration,
'providers': {}
}
# Save provider registrations
for name, reg in self._registered_providers.items():
data['providers'][name] = {
'registered_at': reg.registered_at,
'is_active': reg.is_active,
'settings_schema': reg.settings_schema,
'supports_countries': reg.supports_countries,
'available_countries': reg.available_countries
}
success = self.vfs.write_json(self.settings_file, data)
if success:
logger.debug("Successfully saved unified settings configuration")
else:
logger.error("Failed to save unified settings configuration")
return success
except Exception as e:
logger.error(f"Error saving unified settings configuration: {e}")
return False
# ============= Authentication Manager Interface =============
# These methods provide the clean interface that BaseAuthenticator expects
def get_provider_credentials(self, provider_name: str, country: Optional[str] = None) -> Optional[BaseCredentials]:
"""
Get credentials for a provider with optional country and automatic Kodi sync
Args:
provider_name: Name of the provider
country: Optional country code (e.g., 'de', 'at', 'ch')
Returns:
BaseCredentials instance or None
"""
country_str = f" (country: {country})" if country else ""
logger.debug(f"SettingsManager: Loading credentials for '{provider_name}{country_str}'")
# Try file-based credentials first
credentials = self.credential_manager.load_credentials(provider_name, country)
logger.debug(f"SettingsManager: File credentials result: {type(credentials)}")
if credentials:
logger.debug(f"SettingsManager: File credentials type: {credentials.credential_type}")
if hasattr(credentials, 'username'):
logger.debug(f"SettingsManager: File credentials username: {credentials.username}")
else:
logger.debug(f"SettingsManager: File credentials class: {credentials.__class__.__name__}")
else:
logger.debug(f"SettingsManager: No file credentials found for {provider_name}{country_str}")
# If no file credentials and Kodi is available, try syncing from Kodi
if not credentials and self.kodi_bridge and self.kodi_bridge.is_kodi_environment():
logger.debug(f"SettingsManager: No file credentials for {provider_name}{country_str}, trying Kodi sync")
if self._sync_credentials_from_kodi(provider_name, country):
credentials = self.credential_manager.load_credentials(provider_name, country)
logger.debug(f"SettingsManager: After Kodi sync, credentials: {type(credentials)}")
if credentials:
logger.debug(f"SettingsManager: Synced credentials type: {credentials.credential_type}")
logger.debug(f"SettingsManager: Final credentials for {provider_name}{country_str}: {credentials is not None}")
return credentials
def save_provider_credentials(self, provider_name: str, credentials: BaseCredentials,
country: Optional[str] = None) -> bool:
"""
Save credentials for a provider to both file and optionally Kodi
Args:
provider_name: Name of the provider
credentials: Credentials to save
country: Optional country code
Returns:
True if successful, False otherwise
"""
# Save to file first
file_success = self.credential_manager.save_credentials(provider_name, credentials, country)
if not file_success:
country_str = f" (country: {country})" if country else ""
logger.error(f"Failed to save credentials to file for {provider_name}{country_str}")
return False
# Sync to Kodi if available and in Kodi environment
if self.kodi_bridge and self.kodi_bridge.is_kodi_environment():
kodi_success = self.kodi_bridge.write_credentials_to_kodi(provider_name, credentials, country)
if not kodi_success:
logger.warning(f"Failed to sync credentials to Kodi for {provider_name}")
# Don't fail the entire operation if Kodi sync fails
return True
def load_token_data(self, provider_name: str, country: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Load token data for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
Dictionary with token data or None
"""
return self.session_manager.load_token_data(provider_name, country)
def save_token_data(self, provider_name: str, token_data: Dict[str, Any],
country: Optional[str] = None) -> bool:
"""
Save token data for a provider
Args:
provider_name: Name of the provider
token_data: Token data to save
country: Optional country code
Returns:
True if successful, False otherwise
"""
return self.session_manager.save_session(provider_name, token_data, country)
def get_device_id(self, provider_name: str, country: Optional[str] = None) -> str:
"""
Get or generate device ID for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
Device ID string
"""
return self.session_manager.get_device_id(provider_name, country)
def clear_token(self, provider_name: str, country: Optional[str] = None) -> bool:
"""
Clear token data for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
True if successful, False otherwise
"""
return self.session_manager.clear_token(provider_name, country)
# ============= Provider Management =============
def register_provider(self, provider_name: str, settings_schema: Optional[Dict[str, Any]] = None,
supports_countries: bool = False,
available_countries: Optional[List[str]] = None) -> bool:
"""
Register a provider with the settings manager
Args:
provider_name: Name of the provider to register
settings_schema: DEPRECATED - no longer used (kept for backward compatibility)
supports_countries: Whether this provider supports country-specific settings
available_countries: List of supported country codes (e.g., ['de', 'at', 'ch'])
Returns:
True if successful, False otherwise
"""
try:
registration = ProviderRegistration(
name=provider_name,
is_active=True,
settings_schema=settings_schema, # Keep for backward compat, but not used
supports_countries=supports_countries,
available_countries=available_countries or []
)
self._registered_providers[provider_name] = registration
# Schema registration is no longer needed - Kodi bridge uses convention
# if self.kodi_bridge:
# self.kodi_bridge.register_provider_schema(provider_name, settings_schema) # ← REMOVED
# Save configuration
success = self._save_configuration()
if success:
country_info = f" (supports countries: {available_countries})" if supports_countries else ""
logger.info(f"Successfully registered provider: {provider_name}{country_info}")
return success
except Exception as e:
logger.error(f"Error registering provider {provider_name}: {e}")
return False
def unregister_provider(self, provider_name: str, cleanup_data: bool = False) -> bool:
"""
Unregister a provider and optionally clean up its data
Args:
provider_name: Name of the provider to unregister
cleanup_data: Whether to delete all data for this provider
Returns:
True if successful, False otherwise
"""
if provider_name not in self._registered_providers:
logger.warning(f"Provider {provider_name} is not registered")
return True
try:
# Cleanup data if requested
if cleanup_data:
# Clear all country data if provider supports countries
reg = self._registered_providers[provider_name]
if reg.supports_countries:
for country in reg.available_countries:
self.credential_manager.delete_credentials(provider_name, country)
self.session_manager.clear_session(provider_name, country)
self.proxy_manager.remove_proxy_config(provider_name, country)
else:
# Clear non-country-specific data
self.credential_manager.delete_credentials(provider_name)
self.session_manager.clear_session(provider_name)
self.proxy_manager.remove_proxy_config(provider_name)
# Remove from registry
del self._registered_providers[provider_name]
# Save configuration
success = self._save_configuration()
if success:
logger.info(f"Successfully unregistered provider: {provider_name}")
return success
except Exception as e:
logger.error(f"Error unregistering provider {provider_name}: {e}")
return False
def list_registered_providers(self) -> List[str]:
"""Get list of all registered providers"""
return [name for name, reg in self._registered_providers.items() if reg.is_active]
def is_provider_registered(self, provider_name: str) -> bool:
"""Check if a provider is registered"""
return provider_name in self._registered_providers and self._registered_providers[provider_name].is_active
def get_provider_countries(self, provider_name: str) -> List[str]:
"""
Get list of available countries for a provider
Args:
provider_name: Name of the provider
Returns:
List of country codes, empty if provider doesn't support countries
"""
if provider_name not in self._registered_providers:
return []
reg = self._registered_providers[provider_name]
return reg.available_countries if reg.supports_countries else []
def provider_supports_countries(self, provider_name: str) -> bool:
"""
Check if a provider supports country-specific settings
Args:
provider_name: Name of the provider
Returns:
True if provider supports countries, False otherwise
"""
if provider_name not in self._registered_providers:
return False
return self._registered_providers[provider_name].supports_countries
# ============= Configuration Status =============
def get_provider_status(self, provider_name: str, country: Optional[str] = None) -> Dict[str, Any]:
"""
Get comprehensive status for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
Dictionary with provider status information
"""
if not self.is_provider_registered(provider_name):
return {
'provider_name': provider_name,
'country': country,
'is_registered': False,
'error': 'Provider not registered'
}
registration = self._registered_providers[provider_name]
# Check credential status
credentials = self.get_provider_credentials(provider_name, country)
has_credentials = credentials is not None
credentials_valid = credentials.validate() if credentials else False
# Check session status
token_data = self.load_token_data(provider_name, country)
has_active_session = token_data is not None
# Check proxy status
proxy_config = self.proxy_manager.get_proxy_config(provider_name, country)
has_proxy = proxy_config is not None
status = {
'provider_name': provider_name,
'country': country,
'is_registered': True,
'registered_at': registration.registered_at,
'supports_countries': registration.supports_countries,
'credentials': {
'has_credentials': has_credentials,
'credentials_valid': credentials_valid,
'credential_type': credentials.credential_type if credentials else None
},
'session': {
'has_active_session': has_active_session,
'device_id': self.get_device_id(provider_name, country)
},
'proxy': {
'has_proxy': has_proxy,
'proxy_enabled': has_proxy
},
'kodi_integration': {
'enabled': self.enable_kodi_integration,
'environment': self.kodi_bridge.is_kodi_environment() if self.kodi_bridge else False
}
}
# Add country-specific info if applicable
if registration.supports_countries:
status['available_countries'] = registration.available_countries
# Get session data for all countries
all_countries = self.session_manager.get_all_countries(provider_name)
status['configured_countries'] = all_countries
return status
def is_provider_ready(self, provider_name: str, country: Optional[str] = None) -> bool:
"""
Check if provider is fully configured and ready to use
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
True if ready, False otherwise
"""
status = self.get_provider_status(provider_name, country)
if not status.get('is_registered'):
return False
# Provider is ready if it has valid credentials
credentials_status = status.get('credentials', {})
return credentials_status.get('has_credentials', False) and credentials_status.get('credentials_valid', False)
def get_ready_providers(self, country: Optional[str] = None) -> List[str]:
"""
Get list of providers that are fully configured and ready
Args:
country: Optional country code to check
Returns:
List of provider names
"""
ready = []
for provider_name in self.list_registered_providers():
if self.is_provider_ready(provider_name, country):
ready.append(provider_name)
return ready
# ============= Kodi Integration =============
def _sync_credentials_from_kodi(self, provider_name: str, country: Optional[str] = None) -> bool:
"""
Sync credentials from Kodi for a single provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
True if successful or no sync needed, False on error
"""
country_str = f" (country: {country})" if country else ""
logger.debug(f"SettingsManager: Attempting Kodi sync for {provider_name}{country_str}")
if not self.kodi_bridge or not self.kodi_bridge.is_kodi_environment():
logger.debug(f"SettingsManager: Kodi bridge not available for {provider_name}{country_str}")
return False
try:
kodi_credentials = self.kodi_bridge.read_credentials_from_kodi(provider_name, country)
logger.debug(f"SettingsManager: Kodi credentials result: {type(kodi_credentials)}")
if not kodi_credentials:
logger.debug(f"SettingsManager: No Kodi credentials found for {provider_name}{country_str}")
return True # Not an error
if not kodi_credentials.validate():
logger.debug(f"SettingsManager: Invalid Kodi credentials for {provider_name}{country_str}")
return True # Not an error
# Check if different from current file credentials
file_credentials = self.credential_manager.load_credentials(provider_name, country)
# ← FIX: Only skip if BOTH exist AND are equal
if file_credentials and self._credentials_equal(kodi_credentials, file_credentials):
logger.debug(f"SettingsManager: Credentials already in sync for {provider_name}{country_str}")
return True
# Save Kodi credentials to file
logger.info(f"SettingsManager: Saving credentials from Kodi for {provider_name}{country_str}")
success = self.credential_manager.save_credentials(provider_name, kodi_credentials, country)
logger.debug(f"SettingsManager: Kodi sync save result: {success}")
if success:
logger.info(f"Synced credentials from Kodi for {provider_name}{country_str}")
return success
except Exception as e:
logger.error(f"Error syncing credentials from Kodi for {provider_name}{country_str}: {e}")
return False
def sync_all_from_kodi(self) -> Dict[str, bool]:
"""
Sync all provider settings from Kodi (both single and multi-country)
Returns:
Dictionary mapping provider names (or provider_country) to sync success status
"""
if not self.kodi_bridge or not self.kodi_bridge.is_kodi_environment():
logger.debug("Not in Kodi environment, skipping sync")
return {}
results = {}
# Detect all providers from Kodi
detected_providers = self.kodi_bridge.detect_all_providers_from_kodi()
for provider_name, countries in detected_providers.items():
# Auto-register if not registered
if not self.is_provider_registered(provider_name):
self.register_provider(
provider_name,
supports_countries=bool(countries),
available_countries=countries if countries else []
)
# Sync based on whether it's multi-country or not
if countries:
# Multi-country provider
for country in countries:
cred_success = self._sync_credentials_from_kodi(provider_name, country)
proxy_success = self._sync_proxy_from_kodi(provider_name, country)
results[f"{provider_name}_{country}"] = cred_success or proxy_success
else:
# Single provider
cred_success = self._sync_credentials_from_kodi(provider_name)
proxy_success = self._sync_proxy_from_kodi(provider_name)
results[provider_name] = cred_success or proxy_success
return results
@staticmethod
def _credentials_equal(cred1: Optional[BaseCredentials], cred2: Optional[BaseCredentials]) -> bool:
"""Compare two credentials for equality"""
if cred1 is None and cred2 is None:
return True
if cred1 is None or cred2 is None:
return False
if type(cred1) != type(cred2):
return False
# Use the credential objects' own comparison logic if available
if hasattr(cred1, '__eq__'):
return cred1 == cred2
# Fallback to basic attribute comparison
return getattr(cred1, '__dict__', {}) == getattr(cred2, '__dict__', {})
# ============= Proxy Configuration =============
def get_provider_proxy(self, provider_name: str, country: Optional[str] = None) -> Optional[ProxyConfig]:
"""Get proxy configuration for a provider with Kodi sync"""
# Try proxy manager first
proxy_config = self.proxy_manager.get_proxy_config(provider_name, country)
# If no proxy config and Kodi available, sync from Kodi
if not proxy_config and self.kodi_bridge and self.kodi_bridge.is_kodi_environment():
if self._sync_proxy_from_kodi(provider_name, country):
proxy_config = self.proxy_manager.get_proxy_config(provider_name, country)
return proxy_config
def _sync_proxy_from_kodi(self, provider_name: str, country: Optional[str] = None) -> bool:
"""Sync proxy config from Kodi to proxy manager"""
if not self.kodi_bridge:
return False
kodi_proxy_config = self.kodi_bridge.read_proxy_config_from_kodi(provider_name, country)
if kodi_proxy_config:
return self.proxy_manager.set_proxy_config(provider_name, kodi_proxy_config, country)
return False
def set_provider_proxy(self, provider_name: str, proxy_config: ProxyConfig,
country: Optional[str] = None) -> bool:
"""Set proxy configuration for a provider with Kodi sync"""
success = self.proxy_manager.set_proxy_config(provider_name, proxy_config, country)
# Also sync to Kodi if available
if success and self.kodi_bridge and self.kodi_bridge.is_kodi_environment():
self.kodi_bridge.write_proxy_config_to_kodi(provider_name, proxy_config, country)
return success
def remove_provider_proxy(self, provider_name: str, country: Optional[str] = None) -> bool:
"""Remove proxy configuration for a provider"""
return self.proxy_manager.remove_proxy_config(provider_name, country)
# ============= Session Management =============
def clear_provider_session(self, provider_name: str, country: Optional[str] = None) -> bool:
"""Clear all session data for a provider"""
return self.session_manager.clear_session(provider_name, country)
def get_provider_session_info(self, provider_name: str, country: Optional[str] = None) -> Dict[str, Any]:
"""Get session information for a provider"""
session_data = self.session_manager.load_session(provider_name, country)
if not session_data:
return {
'provider_name': provider_name,
'country': country,
'has_session': False
}
return {
'provider_name': provider_name,
'country': country,
'has_session': True,
'device_id': session_data.get('device_id'),
'has_token': 'access_token' in session_data,
'token_expires_in': session_data.get('expires_in'),
'token_issued_at': session_data.get('issued_at')
}
# ============= Bulk Operations =============
def reset_provider(self, provider_name: str,
reset_credentials: bool = True,
reset_session: bool = True,
reset_proxy: bool = True,
country: Optional[str] = None) -> bool:
"""
Reset various data for a provider
Args:
provider_name: Name of the provider
reset_credentials: Reset stored credentials
reset_session: Reset session data (tokens, device ID)
reset_proxy: Reset proxy configuration
country: Optional country code (if None, resets all countries for multi-country providers)
Returns:
True if successful, False otherwise
"""
if not self.is_provider_registered(provider_name):
logger.error(f"Provider {provider_name} is not registered")
return False
success = True
try:
# If provider supports countries and no specific country given, reset all
if country is None and self.provider_supports_countries(provider_name):
countries = self.get_provider_countries(provider_name)
for ctry in countries:
if reset_credentials:
self.credential_manager.delete_credentials(provider_name, ctry)
if reset_session:
self.session_manager.clear_session(provider_name, ctry)
if reset_proxy:
self.proxy_manager.remove_proxy_config(provider_name, ctry)
logger.info(f"Reset all countries for {provider_name}")
else:
# Reset specific country or non-country provider
if reset_credentials:
self.credential_manager.delete_credentials(provider_name, country)
logger.info(f"Reset credentials for {provider_name}" +
(f" ({country})" if country else ""))
if reset_session:
self.session_manager.clear_session(provider_name, country)
logger.info(f"Reset session for {provider_name}" +
(f" ({country})" if country else ""))
if reset_proxy:
self.proxy_manager.remove_proxy_config(provider_name, country)
logger.info(f"Reset proxy config for {provider_name}" +
(f" ({country})" if country else ""))
except Exception as e:
logger.error(f"Error resetting provider {provider_name}: {e}")
success = False
return success
def export_provider_settings(self, provider_name: str, country: Optional[str] = None) -> Dict[str, Any]:
"""
Export all settings for a single provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
Dictionary with all provider settings
"""
if not self.is_provider_registered(provider_name):
return {'error': 'Provider not registered'}
export_data: Dict[str, Any] = {
'provider_name': provider_name,
'country': country,
'exported_at': time.time(),
'registration': self._registered_providers[provider_name].__dict__,
'status': self.get_provider_status(provider_name, country)
}
# Export credentials (without sensitive data)
credentials = self.get_provider_credentials(provider_name, country)
if credentials:
cred_export = credentials.__dict__.copy()
# Mask sensitive fields
for cred_field in ['password', 'client_secret']:
if cred_field in cred_export:
cred_export[cred_field] = '***MASKED***'
export_data['credentials'] = cred_export
# Export proxy config
proxy_config = self.get_provider_proxy(provider_name, country)
if proxy_config:
export_data['proxy'] = proxy_config.to_dict()
# Export session info (without tokens)
session_info = self.get_provider_session_info(provider_name, country)
export_data['session'] = session_info
# If provider supports countries, export all countries
if country is None and self.provider_supports_countries(provider_name):
countries_data = {}
for ctry in self.get_provider_countries(provider_name):
countries_data[ctry] = self.export_provider_settings(provider_name, ctry)
export_data['countries'] = countries_data
return export_data
def export_all_settings(self, export_path: Optional[str] = None) -> str:
"""
Export all settings to a file
Args:
export_path: Optional path for export file
Returns:
Path to exported file
"""
if not export_path:
timestamp = int(time.time())
# Use config_path for backward compatibility
if self.config_path:
export_path = str(self.config_path / f'settings_export_{timestamp}.json')
else:
export_path = f'settings_export_{timestamp}.json'
export_data: Dict[str, Any] = {
'metadata': {
'exported_at': time.time(),
'version': '2.1',
'source': 'unified_settings_manager_v2.1_country_aware'
},
'system_info': {
'config_dir': str(self.config_path) if self.config_path else 'default_vfs_path',
'kodi_integration_enabled': self.enable_kodi_integration,
'registered_provider_count': len(self._registered_providers)
},
'providers': {}
}
# Export each provider
for provider_name in self.list_registered_providers():
export_data['providers'][provider_name] = self.export_provider_settings(provider_name)
# Write to file - use standard filesystem for exports (not VFS)
with open(export_path, 'w', encoding='utf-8') as f:
json.dump(export_data, f, indent=2, ensure_ascii=False, default=str)
logger.info(f"Exported all settings to: {export_path}")
return export_path
# ============= System Information =============
def get_system_status(self) -> Dict[str, Any]:
"""Get comprehensive system status"""
# Handle config directory status safely - use config_path for backward compatibility
if self.config_path:
config_status = {
'path': str(self.config_path),
'exists': self.config_path.exists(),
'writable': os.access(str(self.config_path), os.W_OK) if self.config_path.exists() else False
}
else:
config_status = {
'path': 'default_vfs_path',
'exists': 'unknown',
'writable': 'unknown'
}
return {
'config_directory': config_status,
'providers': {
'registered': len(self._registered_providers),
'active': len(self.list_registered_providers()),
'ready': len(self.get_ready_providers()),
'country_aware': sum(1 for reg in self._registered_providers.values() if reg.supports_countries)
},
'kodi_integration': {
'enabled': self.enable_kodi_integration,
'bridge_available': self.kodi_bridge is not None,
'in_kodi_environment': self.kodi_bridge.is_kodi_environment() if self.kodi_bridge else False
},
'storage': {
'credential_providers': len(self.credential_manager.list_providers()),
'proxy_configs': len(self.proxy_manager.list_proxy_configs())
}
}
def debug_provider(self, provider_name: str, country: Optional[str] = None) -> Dict[str, Any]:
"""
Get detailed debug information for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
Dictionary with debug information
"""
debug_info: Dict[str, Any] = {
'provider_name': provider_name,
'country': country,
'timestamp': time.time()
}
# Registration info
if provider_name in self._registered_providers:
reg = self._registered_providers[provider_name]
debug_info['registration'] = {
'is_registered': True,
'registered_at': reg.registered_at,
'is_active': reg.is_active,
'has_schema': reg.settings_schema is not None,
'supports_countries': reg.supports_countries,
'available_countries': reg.available_countries
}
else:
debug_info['registration'] = {'is_registered': False}
# File system checks - use config_path for backward compatibility
if self.config_path:
debug_info['filesystem'] = {
'config_dir_exists': self.config_path.exists(),
'config_dir_writable': os.access(str(self.config_path), os.W_OK) if self.config_path.exists() else False
}
else:
debug_info['filesystem'] = {
'config_dir_exists': 'using_vfs_default',
'config_dir_writable': 'using_vfs_default'
}
# Component manager status
debug_info['components'] = {
'credential_manager': {
'has_credentials': provider_name in self.credential_manager.list_providers()
},
'session_manager': {
'has_session': self.session_manager.load_session(provider_name, country) is not None,
'device_id': self.session_manager.get_device_id(provider_name, country),
'all_countries': self.session_manager.get_all_countries(provider_name)
},
'proxy_manager': {
'has_proxy': provider_name in self.proxy_manager.list_proxy_configs()
}
}
# Kodi integration status
if self.kodi_bridge:
debug_info['kodi'] = {
'bridge_available': True,
'in_environment': self.kodi_bridge.is_kodi_environment(),
'provider_test': self.kodi_bridge.test_provider_connectivity(provider_name) if hasattr(
self.kodi_bridge, 'test_provider_connectivity') else 'test_unavailable'
}
else:
debug_info['kodi'] = {'bridge_available': False}
return debug_info
# ============= Country-Specific Helper Methods =============
def get_credential_info(self, provider_name: str, country: Optional[str] = None) -> Dict[str, Any]:
"""
Get information about credentials for a provider
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
Dictionary with credential information
"""
credentials = self.get_provider_credentials(provider_name, country)
return {
'provider_name': provider_name,
'country': country,
'has_credentials': credentials is not None,
'credentials_valid': credentials.validate() if credentials else False,
'credential_type': credentials.credential_type if credentials else None,
'source': 'file' # Could be extended to track source
}
def list_provider_countries_with_data(self, provider_name: str) -> List[str]:
"""
Get list of countries that actually have session/credential data for a provider
Args:
provider_name: Name of the provider
Returns:
List of country codes with data
"""
if not self.provider_supports_countries(provider_name):
return []
# Get countries with session data
return self.session_manager.get_all_countries(provider_name)
def migrate_to_country_structure(self, provider_name: str, default_country: str) -> bool:
"""
Migrate a non-country provider to country-aware structure
Args:
provider_name: Name of the provider
default_country: Country code to migrate existing data to (e.g., 'de')
Returns:
True if successful, False otherwise
"""
if not self.is_provider_registered(provider_name):
logger.error(f"Provider {provider_name} is not registered")
return False
if self.provider_supports_countries(provider_name):
logger.warning(f"Provider {provider_name} already supports countries")
return True
try:
logger.info(f"Migrating {provider_name} to country structure with default: {default_country}")
# Load existing non-country data
old_credentials = self.credential_manager.load_credentials(provider_name)
old_session = self.session_manager.load_session(provider_name)
old_proxy = self.proxy_manager.get_proxy_config(provider_name)
# Save to country-specific locations
if old_credentials:
self.credential_manager.save_credentials(provider_name, old_credentials, default_country)
logger.debug(f"Migrated credentials to {default_country}")
if old_session:
self.session_manager.save_session(provider_name, old_session, default_country)
logger.debug(f"Migrated session to {default_country}")
if old_proxy:
self.proxy_manager.set_proxy_config(provider_name, old_proxy, default_country)
logger.debug(f"Migrated proxy to {default_country}")
# Clear old non-country data
self.credential_manager.delete_credentials(provider_name, None)
self.session_manager.clear_session(provider_name, None)
self.proxy_manager.remove_proxy_config(provider_name, None)
# Update registration
reg = self._registered_providers[provider_name]
reg.supports_countries = True
reg.available_countries = [default_country]
self._save_configuration()
logger.info(f"Successfully migrated {provider_name} to country structure")
return True
except Exception as e:
logger.error(f"Error migrating {provider_name} to country structure: {e}")
return False
# For imports that expect the old interface
UnifiedSettingsManager = SettingsManager # Backward compatibility alias
__all__ = ['SettingsManager', 'UnifiedSettingsManager', 'ProviderRegistration']