# streaming_providers/base/provider_registry.py """ Core provider registry handling discovery, metadata, and lifecycle management. """ from typing import Any, Dict, List, Optional from .provider import StreamingProvider from .utils.logger import logger class ProviderMetadata: """Metadata for a provider instance with lazy initialization.""" def __init__(self, plugin_class, country: str, enabled: bool = False): self.plugin_class = plugin_class self.country = country.lower() self.enabled = enabled self.instance: Optional[StreamingProvider] = None self._extract_metadata() def _extract_metadata(self): """Extract static metadata from provider class without instantiation""" self.plugin_name = self.plugin_class.__name__.lower().replace("provider", "") # Check if provider supports multiple countries supports_multiple = self.plugin_class.supports_multiple_countries() if supports_multiple: # Multi-country provider: include country in name self.name = f"{self.plugin_name}_{self.country}" self.is_multi_country = True else: # Single-country provider self.name = self.plugin_name # Check if provider has an explicit single country supported_countries = self.plugin_class.get_static_supported_countries() if len(supported_countries) == 1: # Provider has exactly one supported country (e.g., HRTi with ["HR"]) # Use that country instead of the passed-in country self.country = supported_countries[0].upper() self.is_multi_country = False else: # True single-country or country-agnostic (empty list) self.is_multi_country = False # Rest of the method remains the same... self.label = self.plugin_class.get_static_label(self.country) self.supported_auth_types = self.plugin_class.get_static_auth_types() self.logo = self.plugin_class.get_static_logo(self.country) self.supported_countries = self.plugin_class.get_static_supported_countries() self.requires_credentials = any( auth_type in ["user_credentials", "client_credentials"] for auth_type in self.supported_auth_types ) def create_instance(self) -> Optional[StreamingProvider]: """Lazily create provider instance if enabled""" if not self.enabled: return None if self.instance is None: try: logger.info(f"Creating instance for provider: {self.name}") self.instance = self.plugin_class(country=self.country) logger.debug(f"Successfully created instance for {self.name}") except Exception as e: logger.error(f"Failed to create instance for {self.name}: {e}") self.instance = None return self.instance def destroy_instance(self): """Clean up provider instance""" if self.instance: logger.debug(f"Destroying instance for provider: {self.name}") self.instance = None def set_enabled(self, enabled: bool): """Update enabled status and manage instance accordingly""" self.enabled = enabled if enabled and self.instance is None: self.create_instance() elif not enabled and self.instance is not None: self.destroy_instance() def to_dict(self) -> Dict[str, Any]: """Convert metadata to dictionary for API response""" return { "name": self.name, "label": self.label, "plugin": self.plugin_name, "country": self.country.upper(), "enabled": self.enabled, "instance_ready": self.instance is not None, "requires_credentials": self.requires_credentials, "supported_auth_types": self.supported_auth_types, "logo": self.logo, "is_multi_country": self.is_multi_country, "supported_countries": self.supported_countries, } class M3UGroupMetadata: """ Specialized metadata for M3U group-based providers. NOW SUPPORTS LAZY INITIALIZATION like regular ProviderMetadata. This ensures M3U providers get proper proxy configuration. """ def __init__(self, plugin_class, group: str, country: str, enabled: bool = False): """ Initialize M3U group metadata. Args: plugin_class: M3UProvider class (not instance!) group: Group title from M3U file country: Country code enabled: Whether this provider is enabled """ self.plugin_class = plugin_class self.group = group self.country = country.lower() self.enabled = enabled self.instance: Optional[StreamingProvider] = None # Extract metadata from class self._extract_metadata() def _extract_metadata(self): """Extract metadata from provider class""" self.plugin_name = "m3u" # Create name from sanitized group title # IMPORTANT: This MUST match what M3UProvider.provider_name property returns! # See M3UProvider lines 189-194: # clean_name = re.sub(r'[^a-z0-9\s]+', '', self.group_filter.lower()) # clean_name = re.sub(r'\s+', '_', clean_name).strip('_') import re clean_name = re.sub(r'[^a-z0-9\s]+', '', self.group.lower()) clean_name = re.sub(r'\s+', '_', clean_name).strip('_') self.name = clean_name if clean_name else "m3u" # Static metadata self.label = self.group # Use original group as display label self.is_multi_country = False self.supported_auth_types = self.plugin_class.get_static_auth_types() self.logo = self.plugin_class.get_static_logo() self.supported_countries = ["*"] # M3U supports all countries self.requires_credentials = False def create_instance(self) -> Optional[StreamingProvider]: """ Lazily create M3U provider instance. This now follows the same pattern as regular providers, ensuring proper initialization including proxy configuration. """ if not self.enabled: return None if self.instance is None: try: logger.info(f"Creating M3U group instance: {self.name} (group: {self.group})") # Create instance with group filter - __init__ will handle proxy setup self.instance = self.plugin_class( country=self.country, group_filter=self.group ) logger.debug(f"Successfully created instance for M3U group: {self.name}") except Exception as e: logger.error(f"Failed to create M3U group instance {self.name}: {e}") self.instance = None return self.instance def destroy_instance(self): """Clean up provider instance""" if self.instance: logger.debug(f"Destroying M3U group instance: {self.name}") self.instance = None def set_enabled(self, enabled: bool): """Update enabled status and manage instance accordingly""" self.enabled = enabled if enabled and self.instance is None: self.create_instance() elif not enabled and self.instance is not None: self.destroy_instance() def to_dict(self) -> Dict[str, Any]: """Convert metadata to dictionary for API response""" return { "name": self.name, "label": self.label, "plugin": self.plugin_name, "country": self.country.upper(), "enabled": self.enabled, "instance_ready": self.instance is not None, "requires_credentials": self.requires_credentials, "supported_auth_types": self.supported_auth_types, "logo": self.logo, "is_multi_country": self.is_multi_country, "supported_countries": self.supported_countries, # M3U-specific fields "group": self.group, "type": "m3u_group", } class ProviderRegistry: """ Core registry for provider discovery, metadata, and lifecycle management. Separated concern: Provider registration and access. """ def __init__(self): self.providers: Dict[str, StreamingProvider] = {} # Active instances self.provider_metadata: Dict[str, ProviderMetadata] = {} # All providers logger.info("ProviderRegistry: Initialized") @staticmethod def _is_provider_enabled(provider_name: str, country: Optional[str] = None) -> bool: """Check if a provider is enabled via settings manager.""" try: from .settings.provider_enable_manager import ProviderEnableManager enable_manager = ProviderEnableManager() instance_name = f"{provider_name}_{country}" if country else provider_name return enable_manager.is_provider_enabled(instance_name) except Exception as e: logger.warning(f"Could not check enable status for '{provider_name}': {e}") return True def discover_all_providers(self, default_country: str = "DE") -> List[str]: """Discover ALL provider instances and extract metadata.""" from streaming_providers import AVAILABLE_PROVIDERS logger.info("ProviderRegistry: Discovering provider instances") discovered = [] for plugin_name, plugin_class in AVAILABLE_PROVIDERS.items(): # Special handling for M3U provider - discover groups if plugin_name == "m3u": discovered_groups = self._discover_m3u_groups(plugin_class, default_country) discovered.extend(discovered_groups) continue if plugin_class.supports_multiple_countries(): for country in plugin_class.get_static_supported_countries(): instance_name = f"{plugin_name}_{country}" enabled = self._is_provider_enabled(plugin_name, country) metadata = ProviderMetadata(plugin_class, country, enabled) self.provider_metadata[instance_name] = metadata discovered.append(instance_name) if enabled: instance = metadata.create_instance() if instance: self.providers[instance_name] = instance else: instance_name = plugin_name enabled = self._is_provider_enabled(plugin_name) metadata = ProviderMetadata(plugin_class, default_country, enabled) self.provider_metadata[instance_name] = metadata discovered.append(instance_name) if enabled: instance = metadata.create_instance() if instance: self.providers[instance_name] = instance logger.info(f"ProviderRegistry: Discovered {len(discovered)} provider instances") return discovered def _discover_m3u_groups(self, plugin_class, default_country: str) -> List[str]: """ Discover M3U groups and register each as a separate provider instance. Each group-title in M3U files becomes an independent provider. IMPORTANT: This now creates lazy metadata instead of eager instances, allowing proper proxy configuration during initialization. Args: plugin_class: M3UProvider class default_country: Default country code Returns: List of discovered provider instance names """ discovered = [] try: # Import M3UProvider to call static discovery method from streaming_providers.providers.m3u import M3UProvider # Discover all unique groups from M3U files # Now includes filename-based fallback for files without group-title groups = M3UProvider.discover_groups(config_dir=None) if not groups: logger.warning("M3U: No M3U files found or all files are unreadable") return discovered logger.info(f"M3U: Discovered {len(groups)} groups: {', '.join(groups)}") # Check if M3U provider is enabled # Use base "m3u" for enable check (all groups share same enable status) enabled = self._is_provider_enabled("m3u", None) # Register each group as a separate provider with LAZY METADATA for group in groups: try: # Create lazy metadata (no instance yet!) metadata = M3UGroupMetadata( plugin_class=plugin_class, # Pass class, not instance group=group, country=default_country, enabled=enabled ) # Register metadata self.provider_metadata[metadata.name] = metadata discovered.append(metadata.name) # Only create instance if enabled if enabled: instance = metadata.create_instance() if instance: self.providers[metadata.name] = instance logger.debug( f"M3U: Registered group provider '{metadata.name}' " f"(label: {group}, enabled: {enabled})" ) except Exception as e: logger.error(f"M3U: Failed to register group '{group}': {e}") continue logger.info(f"M3U: Successfully registered {len(discovered)} group providers") except ImportError: logger.warning("M3U: M3UProvider not available for group discovery") except Exception as e: logger.error(f"M3U: Error during group discovery: {e}") return discovered def get_provider(self, provider_name: str) -> Optional[StreamingProvider]: """Get provider instance, creating it lazily if needed.""" provider = self.providers.get(provider_name) if provider: return provider metadata = self.provider_metadata.get(provider_name) if not metadata or not metadata.enabled: return None provider = metadata.create_instance() if provider: self.providers[provider_name] = provider return provider def set_provider_enabled(self, provider_name: str, enabled: bool) -> bool: """Enable or disable a provider dynamically.""" metadata = self.provider_metadata.get(provider_name) if not metadata: logger.error(f"Cannot enable/disable unknown provider '{provider_name}'") return False metadata.set_enabled(enabled) if enabled and metadata.instance: self.providers[provider_name] = metadata.instance elif not enabled and provider_name in self.providers: del self.providers[provider_name] try: from .settings.provider_enable_manager import ProviderEnableManager enable_manager = ProviderEnableManager() success, message = enable_manager.set_provider_enabled(provider_name, enabled) return success except Exception as e: logger.error(f"Error updating enable status: {e}") return False def reinitialize_provider(self, provider_name: str) -> bool: """Reinitialize a provider instance.""" metadata = self.provider_metadata.get(provider_name) if not metadata or not metadata.enabled: return False try: metadata.destroy_instance() new_instance = metadata.create_instance() if new_instance: self.providers[provider_name] = new_instance return True return False except Exception as e: logger.error(f"Failed to reinitialize '{provider_name}': {e}") return False def get_all_providers_metadata(self) -> List[Dict[str, Any]]: """Get metadata for ALL provider instances.""" return [m.to_dict() for m in self.provider_metadata.values()] def list_providers(self) -> List[str]: """List enabled provider names.""" return list(self.providers.keys()) def list_all_providers(self) -> List[str]: """List ALL provider names (enabled + disabled).""" return list(self.provider_metadata.keys()) def get_enabled_providers(self) -> List[str]: """Get list of enabled provider names.""" return [name for name, m in self.provider_metadata.items() if m.enabled] def clear_providers(self): """Clear all providers.""" self.providers.clear() logger.info("ProviderRegistry: Cleared all providers")