# streaming_providers/base/network/proxy_manager.py import time from pathlib import Path from typing import Any, Dict, List, Optional from ..models.proxy_models import ProxyConfig, ProxyScope from ..utils.logger import logger class ProxyConfigManager: """ Manages proxy configurations with file-based persistence Supports per-provider proxy settings with global fallbacks Now supports country-specific proxy configurations """ def __init__(self, config_dir: Optional[str] = None): """ Initialize proxy configuration manager with VFS support """ # Initialize VFS with config directory support from ..utils.vfs import VFS self.vfs = VFS(config_dir=config_dir) # Keep legacy attributes for backward compatibility but use VFS primarily if config_dir: self.config_dir = Path(config_dir) else: self.config_dir = Path.home() / ".streaming_providers" / "config" self.proxy_config_file = "proxy_config.json" # Use relative path for VFS # Cache for loaded configurations self._config_cache: Dict[str, ProxyConfig] = {} self._global_config: Optional[ProxyConfig] = None # Load existing configurations self._load_configurations() @staticmethod def _get_proxy_path(provider_name: str, country: Optional[str] = None) -> tuple: """ Determine the path to proxy data based on country Args: provider_name: Provider name country: Optional country code (e.g., 'de', 'at', 'ch') Returns: Tuple of (cache_key, is_nested) """ if country: return f"{provider_name}_{country}", True else: return provider_name, False def _load_configurations(self) -> None: """Load proxy configurations from file using VFS""" try: vfs_data = self.vfs.read_json(self.proxy_config_file) if vfs_data is None: logger.debug("No proxy configuration file found, starting with empty config") return # Load global configuration if "global" in vfs_data: self._global_config = ProxyConfig.from_dict(vfs_data["global"]) logger.debug("Loaded global proxy configuration") # Load provider-specific configurations providers = vfs_data.get("providers", {}) for provider_name, provider_data in providers.items(): try: # Check if this is a nested (country-aware) structure if isinstance(provider_data, dict) and any( isinstance(v, dict) and len(k) <= 3 for k, v in provider_data.items() ): # Country-aware structure for country, config_data in provider_data.items(): if isinstance(config_data, dict) and len(country) <= 3: cache_key = f"{provider_name}_{country}" self._config_cache[cache_key] = ProxyConfig.from_dict(config_data) logger.debug(f"Loaded proxy config for {provider_name} ({country})") else: # Flat structure (no country) self._config_cache[provider_name] = ProxyConfig.from_dict(provider_data) logger.debug(f"Loaded proxy configuration for provider: {provider_name}") except Exception as e: logger.error(f"Error loading proxy config for {provider_name}: {e}") except Exception as e: logger.error(f"Error loading proxy configurations: {e}") def _save_configurations(self) -> bool: """Save proxy configurations to file using VFS""" try: data: Dict[str, Any] = { "providers": {}, "metadata": { "version": "1.1", # Bumped for country support "description": "Streaming provider proxy configurations", }, } # Save global configuration if self._global_config: data["global"] = self._global_config.to_dict() # Save provider-specific configurations # Group by provider to maintain nested structure provider_configs: Dict[str, Dict] = {} for cache_key, config in self._config_cache.items(): if "_" in cache_key and len(cache_key.split("_")[-1]) <= 3: # Country-aware key (e.g., "joyn_de") parts = cache_key.rsplit("_", 1) provider_name = parts[0] country = parts[1] if provider_name not in provider_configs: provider_configs[provider_name] = {} provider_configs[provider_name][country] = config.to_dict() else: # Non-country key provider_configs[cache_key] = config.to_dict() data["providers"] = provider_configs # Save using VFS success = self.vfs.write_json(self.proxy_config_file, data) if success: logger.debug("Saved proxy configurations") else: logger.error("Failed to save proxy configurations") return success except Exception as e: logger.error(f"Error saving proxy configurations: {e}") return False def get_proxy_config( self, provider_name: str, country: Optional[str] = None ) -> Optional[ProxyConfig]: """ Get proxy configuration for a provider and optional country Args: provider_name: Provider name country: Optional country code Returns: ProxyConfig or None """ cache_key, _ = self._get_proxy_path(provider_name, country) # Check provider-specific config first if cache_key in self._config_cache: config = self._config_cache[cache_key] country_str = f" ({country})" if country else "" logger.debug( f"Using provider-specific proxy for {provider_name}{country_str}: " f"{config.proxy_type.value}://{config.host}:{config.port}" ) return config # If country specified but not found, try without country if country and provider_name in self._config_cache: config = self._config_cache[provider_name] logger.debug( f"Using non-country proxy for {provider_name} ({country}): " f"{config.proxy_type.value}://{config.host}:{config.port}" ) return config # Fall back to global config if self._global_config: country_str = f" ({country})" if country else "" logger.debug( f"Using global proxy for {provider_name}{country_str}: " f"{self._global_config.proxy_type.value}://{self._global_config.host}:{self._global_config.port}" ) return self._global_config country_str = f" ({country})" if country else "" logger.debug(f"No proxy configuration found for {provider_name}{country_str}") return None def set_proxy_config( self, provider_name: str, proxy_config: ProxyConfig, country: Optional[str] = None, ) -> bool: """ Set proxy configuration for a provider Args: provider_name: Name of the provider (use 'global' for global config) proxy_config: Proxy configuration to set country: Optional country code Returns: True if successful, False otherwise """ if not proxy_config.validate(): logger.error(f"Invalid proxy configuration for {provider_name}") return False try: if provider_name == "global": self._global_config = proxy_config logger.info("Set global proxy configuration") else: cache_key, _ = self._get_proxy_path(provider_name, country) self._config_cache[cache_key] = proxy_config country_str = f" ({country})" if country else "" logger.info(f"Set proxy configuration for provider: {provider_name}{country_str}") return self._save_configurations() except Exception as e: country_str = f" ({country})" if country else "" logger.error(f"Error setting proxy configuration for {provider_name}{country_str}: {e}") return False def remove_proxy_config(self, provider_name: str, country: Optional[str] = None) -> bool: """ Remove proxy configuration for a provider Args: provider_name: Name of the provider (use 'global' for global config) country: Optional country code (if None, removes all countries for provider) Returns: True if successful, False otherwise """ try: if provider_name == "global": self._global_config = None logger.info("Removed global proxy configuration") else: if country: # Remove specific country cache_key, _ = self._get_proxy_path(provider_name, country) if cache_key in self._config_cache: del self._config_cache[cache_key] logger.info(f"Removed proxy configuration for {provider_name} ({country})") else: logger.warning( f"No proxy configuration found for {provider_name} ({country})" ) return True else: # Remove all countries for provider keys_to_remove = [ key for key in self._config_cache.keys() if key == provider_name or key.startswith(f"{provider_name}_") ] if keys_to_remove: for key in keys_to_remove: del self._config_cache[key] logger.info(f"Removed all proxy configurations for {provider_name}") else: logger.warning(f"No proxy configuration found for {provider_name}") return True return self._save_configurations() except Exception as e: country_str = f" ({country})" if country else "" logger.error( f"Error removing proxy configuration for {provider_name}{country_str}: {e}" ) return False def get_all_countries(self, provider_name: str) -> List[str]: """ Get all countries that have proxy configs for a provider Args: provider_name: Provider name Returns: List of country codes """ countries = [] prefix = f"{provider_name}_" for cache_key in self._config_cache.keys(): if cache_key.startswith(prefix): country = cache_key[len(prefix) :] if len(country) <= 3: # Validate it looks like a country code countries.append(country) return countries def list_proxy_configs(self) -> List[str]: """ Get list of providers with proxy configurations Returns: List of provider names (including country-specific ones) """ providers = list(self._config_cache.keys()) if self._global_config: providers.append("global") return providers def test_proxy_config( self, provider_name: str, country: Optional[str] = None ) -> Dict[str, Any]: """ Test proxy configuration for a provider Args: provider_name: Name of the provider country: Optional country code Returns: Dictionary with test results """ proxy_config = self.get_proxy_config(provider_name, country) if not proxy_config: return { "success": False, "error": "No proxy configuration found", "provider": provider_name, "country": country, } # Test basic connectivity through proxy from .http_manager import HTTPManager, RequestConfig config = RequestConfig(proxy_config=proxy_config, provider=provider_name) manager = HTTPManager(config) result = manager.test_connection() result["provider"] = provider_name result["country"] = country result["proxy_config"] = proxy_config.to_dict() manager.close() return result def get_proxy_info(self, provider_name: str, country: Optional[str] = None) -> Dict[str, Any]: """ Get detailed information about proxy configuration Args: provider_name: Name of the provider country: Optional country code Returns: Dictionary with proxy information """ proxy_config = self.get_proxy_config(provider_name, country) cache_key, _ = self._get_proxy_path(provider_name, country) info = { "provider": provider_name, "country": country, "has_proxy": proxy_config is not None, "config_source": None, "proxy_details": None, } if proxy_config: # Determine config source if cache_key in self._config_cache: info["config_source"] = ( "provider_country_specific" if country else "provider_specific" ) elif country and provider_name in self._config_cache: info["config_source"] = "provider_fallback" elif self._global_config: info["config_source"] = "global" # Add proxy details (without sensitive auth info) info["proxy_details"] = { "host": proxy_config.host, "port": proxy_config.port, "proxy_type": proxy_config.proxy_type.value, "has_auth": proxy_config.auth is not None, "scope": proxy_config.scope.__dict__, "timeout": proxy_config.timeout, "verify_ssl": proxy_config.verify_ssl, } return info def export_config(self, export_path: Optional[str] = None) -> str: """ Export proxy configurations to a file Args: export_path: Optional path to export to, defaults to backup file Returns: Path to exported file """ if not export_path: export_path = str(self.config_dir / f"proxy_config_backup_{int(time.time())}.json") try: # Create export data with country-aware structure providers_export = {} for cache_key, config in self._config_cache.items(): if "_" in cache_key and len(cache_key.split("_")[-1]) <= 3: # Country-aware key parts = cache_key.rsplit("_", 1) provider_name = parts[0] country = parts[1] if provider_name not in providers_export: providers_export[provider_name] = {} providers_export[provider_name][country] = config.to_dict() else: # Non-country key providers_export[cache_key] = config.to_dict() export_data = { "metadata": { "exported_at": time.time(), "version": "1.1", "source": "streaming_providers_proxy_manager", }, "global": (self._global_config.to_dict() if self._global_config else None), "providers": providers_export, } success = self.vfs.write_json(export_path, export_data) if not success: raise IOError(f"VFS write_json failed for {export_path}") logger.info(f"Exported proxy configurations to {export_path}") return export_path except Exception as e: logger.error(f"Error exporting proxy configurations: {e}") raise def import_config(self, import_path: str, merge: bool = True) -> bool: """ Import proxy configurations from a file Args: import_path: Path to import file merge: If True, merge with existing configs; if False, replace all Returns: True if successful, False otherwise """ try: import_data = self.vfs.read_json(import_path) if import_data is None: logger.error(f"No data found at {import_path}") return False if not merge: # Clear existing configurations self._config_cache.clear() self._global_config = None # Import global configuration if "global" in import_data and import_data["global"]: self._global_config = ProxyConfig.from_dict(import_data["global"]) # Import provider configurations providers = import_data.get("providers", {}) for provider_name, provider_data in providers.items(): try: # Check if nested (country-aware) if isinstance(provider_data, dict) and any( isinstance(v, dict) and len(k) <= 3 for k, v in provider_data.items() ): # Country-aware structure for country, config_data in provider_data.items(): if isinstance(config_data, dict) and len(country) <= 3: cache_key = f"{provider_name}_{country}" self._config_cache[cache_key] = ProxyConfig.from_dict(config_data) else: # Flat structure self._config_cache[provider_name] = ProxyConfig.from_dict(provider_data) except Exception as e: logger.error(f"Error importing config for {provider_name}: {e}") # Save the imported configurations success = self._save_configurations() if success: logger.info(f"Successfully imported proxy configurations from {import_path}") return success except Exception as e: logger.error(f"Error importing proxy configurations: {e}") return False def create_proxy_from_url( self, provider_name: str, proxy_url: str, scope: Optional[ProxyScope] = None, country: Optional[str] = None, ) -> bool: """ Create and set proxy configuration from URL Args: provider_name: Name of the provider proxy_url: Proxy URL (e.g., "http://user:pass@proxy.example.com:8080") scope: Optional scope configuration country: Optional country code Returns: True if successful, False otherwise """ try: proxy_config = ProxyConfig.from_url(proxy_url, scope) return self.set_proxy_config(provider_name, proxy_config, country) except Exception as e: country_str = f" ({country})" if country else "" logger.error( f"Error creating proxy config from URL for {provider_name}{country_str}: {e}" ) return False def bulk_set_proxy( self, proxy_config: ProxyConfig, provider_names: List[str], country: Optional[str] = None, ) -> Dict[str, bool]: """ Set the same proxy configuration for multiple providers Args: proxy_config: Proxy configuration to set provider_names: List of provider names country: Optional country code to apply to all providers Returns: Dictionary mapping provider names to success status """ results = {} for provider_name in provider_names: results[provider_name] = self.set_proxy_config(provider_name, proxy_config, country) return results def get_all_proxy_info(self) -> Dict[str, Dict[str, Any]]: """ Get proxy information for all configured providers Returns: Dictionary mapping provider names to their proxy info """ info = {} # Add global config info if self._global_config: info["global"] = self.get_proxy_info("global") # Add provider-specific configs for cache_key in self._config_cache.keys(): if "_" in cache_key and len(cache_key.split("_")[-1]) <= 3: # Country-aware key parts = cache_key.rsplit("_", 1) provider_name = parts[0] country = parts[1] info[cache_key] = self.get_proxy_info(provider_name, country) else: # Non-country key info[cache_key] = self.get_proxy_info(cache_key) return info