mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-16 06:02:35 +02:00
1129 lines
46 KiB
Python
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'] |