Files
script.service.ultimate/lib/streaming_providers/base/manager.py
T
2025-12-04 10:22:54 +01:00

573 lines
23 KiB
Python

# streaming_providers/base/manager.py
from typing import Dict, List, Optional
from .provider import StreamingProvider
from .models import StreamingChannel, DRMSystem
from .drm import DRMPluginManager
from .epg import EPGManager
from .utils.logger import logger
class ProviderManager:
"""
Central manager for handling multiple streaming providers.
Handles provider registration, discovery, and channel fetching operations.
"""
def __init__(self):
"""Initialize with empty provider registry and DRM plugin manager"""
self.providers: Dict[str, StreamingProvider] = {}
self.drm_plugin_manager = DRMPluginManager()
logger.info("ProviderManager: Initialized with DRM plugin manager")
self.epg_manager = EPGManager()
logger.info("ProviderManager: Initialized with DRM plugin manager and EPG manager")
@staticmethod
def _is_provider_enabled(provider_name: str, country: Optional[str] = None) -> bool:
"""
Check if a provider is enabled via settings manager.
Args:
provider_name: Name of the provider
country: Optional country code
Returns:
True if provider is enabled, False otherwise
"""
try:
from .settings.settings_manager import SettingsManager
# Create or get existing settings manager
settings_manager = SettingsManager()
return settings_manager.is_provider_enabled(provider_name, country)
except Exception as e:
logger.warning(f"Could not check enable status for '{provider_name}': {e}, defaulting to enabled")
return True
def register_provider(self, provider: StreamingProvider) -> None:
"""
Register a single provider instance.
Args:
provider: Configured provider instance to register
"""
if not isinstance(provider, StreamingProvider):
logger.error(f"ProviderManager: Failed to register provider - invalid type: {type(provider)}")
raise ValueError("Only StreamingProvider instances can be registered")
self.providers[provider.provider_name] = provider
logger.info(f"ProviderManager: Registered provider '{provider.provider_name}'")
def register_providers(self, providers: List[StreamingProvider]) -> None:
"""
Register multiple provider instances at once.
Args:
providers: List of configured provider instances
"""
logger.info(f"ProviderManager: Registering {len(providers)} providers")
for provider in providers:
self.register_provider(provider)
def discover_providers(self, country: str = 'DE', detected_providers: Dict[str, List[str]] = None) -> List[str]:
from streaming_providers import AVAILABLE_PROVIDERS
# Backward compatibility: if no detected_providers, use original discovery logic
if detected_providers is None:
logger.info(f"ProviderManager: Discovering providers for country '{country}'")
registered = []
failed = []
for provider_name, provider_class in AVAILABLE_PROVIDERS.items():
if provider_name not in self.providers:
# CHECK ENABLED STATUS
if not self._is_provider_enabled(provider_name):
logger.debug(f"ProviderManager: Provider '{provider_name}' is disabled, skipping")
continue
try:
provider = provider_class(country=country)
self.register_provider(provider)
registered.append(provider_name)
except Exception as e:
failed.append((provider_name, str(e)))
logger.warning(f"ProviderManager: Could not initialize provider '{provider_name}': {e}")
logger.info(
f"ProviderManager: Discovery completed - {len(registered)} providers registered, {len(failed)} failed")
return registered
# New multi-country logic
logger.info(f"ProviderManager: Discovering providers with multi-country support")
registered = []
failed = []
for provider_name, countries in detected_providers.items():
if provider_name not in AVAILABLE_PROVIDERS:
logger.warning(f"ProviderManager: Provider '{provider_name}' not in AVAILABLE_PROVIDERS, skipping")
continue
provider_class = AVAILABLE_PROVIDERS[provider_name]
try:
if countries: # Multi-country provider
enabled_countries = []
for country_code in countries:
# CHECK COUNTRY-SPECIFIC ENABLED STATUS
if not self._is_provider_enabled(provider_name, country_code):
logger.debug(
f"ProviderManager: Provider '{provider_name}_{country_code}' is disabled, skipping")
continue
provider_key = f"{provider_name}_{country_code}"
if provider_key not in self.providers:
provider = provider_class(country=country_code.lower())
self.providers[provider_key] = provider
enabled_countries.append(country_code)
logger.debug(f"ProviderManager: Registered {provider_key}")
# Only add to registered list if at least one country is enabled
if enabled_countries and provider_name not in registered:
registered.append(provider_name)
else: # Single country provider (fallback to default country)
# CHECK ENABLED STATUS
if not self._is_provider_enabled(provider_name):
logger.debug(f"ProviderManager: Provider '{provider_name}' is disabled, skipping")
continue
if provider_name not in self.providers:
provider = provider_class(country=country.lower())
self.providers[provider_name] = provider
registered.append(provider_name)
except Exception as e:
failed.append((provider_name, str(e)))
logger.warning(f"ProviderManager: Could not initialize provider '{provider_name}': {e}")
logger.info(
f"ProviderManager: Discovery completed - {len(registered)} providers registered, {len(failed)} failed")
return registered
def discover_drm_plugins(self) -> List[str]:
"""
Discover and register all available DRM plugins.
Returns:
List of discovered plugin names
"""
logger.info("ProviderManager: Discovering DRM plugins")
discovered = self.drm_plugin_manager.discover_plugins()
logger.info(f"ProviderManager: DRM plugin discovery completed - {len(discovered)} plugins available")
return discovered
def get_provider(self, provider_name: str) -> Optional[StreamingProvider]:
"""
Get registered provider by name.
Args:
provider_name: Name of the provider to retrieve
Returns:
The provider instance or None if not found
"""
provider = self.providers.get(provider_name)
if not provider:
logger.debug(f"ProviderManager: Provider '{provider_name}' not found")
return provider
def get_provider_http_manager(self, provider_name: str):
"""
Get HTTP manager for a specific provider.
Args:
provider_name: Name of the provider
Returns:
HTTPManager instance if provider exists and has one, None otherwise
"""
provider = self.get_provider(provider_name)
if not provider:
logger.warning(f"ProviderManager: Provider '{provider_name}' not found")
return None
http_manager = provider.http_manager
if not http_manager:
logger.warning(f"ProviderManager: Provider '{provider_name}' has no HTTP manager configured")
return None
logger.debug(f"ProviderManager: Retrieved HTTP manager for provider '{provider_name}'")
return http_manager
def needs_proxy(self, provider_name: str) -> bool:
"""
Check if a provider needs proxy support.
Args:
provider_name: Name of the provider
Returns:
True if provider has proxy configured, False otherwise
"""
http_manager = self.get_provider_http_manager(provider_name)
if not http_manager:
return False
has_proxy = http_manager.config.proxy_config is not None
if has_proxy:
logger.debug(f"ProviderManager: Provider '{provider_name}' requires proxy")
else:
logger.debug(f"ProviderManager: Provider '{provider_name}' does not require proxy")
return has_proxy
def get_provider_choices(self) -> Dict[int, str]:
"""
Get numbered provider choices for user selection.
Includes an 'all' option as the last choice.
Returns:
Mapping of choice numbers to provider names
"""
choices = {i+1: name for i, name in enumerate(self.providers.keys())}
choices[len(choices)+1] = 'all'
return choices
def get_selected_providers(self, choices_input: str) -> List[str]:
"""
Convert user input string to list of provider names.
Args:
choices_input: Comma-separated string of choice numbers
Returns:
List of selected provider names
"""
available = list(self.providers.keys())
if not available:
logger.warning("ProviderManager: No providers available for selection")
return []
selected = []
for choice in choices_input.split(','):
choice = choice.strip()
if choice.isdigit():
idx = int(choice) - 1
if 0 <= idx < len(available):
selected.append(available[idx])
elif idx == len(available): # 'all' option
selected = available.copy()
result = selected or available
logger.debug(f"ProviderManager: Selected {len(result)} providers from input '{choices_input}'")
return result
def get_channels(self, provider_name: str, fetch_manifests: bool = False, **kwargs) -> List[StreamingChannel]:
"""
Get channels from a specific provider.
Args:
provider_name: Name of the provider
fetch_manifests: Whether to enrich channels with manifest data
**kwargs: Additional arguments for channel fetching
Returns:
List of channels from the provider
Raises:
ValueError: If provider not found
"""
provider = self.get_provider(provider_name)
if not provider:
logger.error(f"ProviderManager: Cannot get channels - provider '{provider_name}' not found")
raise ValueError(f"Provider '{provider_name}' not found")
logger.debug(f"ProviderManager: Fetching channels from provider '{provider_name}' (fetch_manifests={fetch_manifests})")
channels = provider.get_channels(**kwargs)
logger.info(f"ProviderManager: Retrieved {len(channels)} channels from provider '{provider_name}'")
if fetch_manifests and not provider.uses_dynamic_manifests:
logger.debug(f"ProviderManager: Enriching channels with manifest data for provider '{provider_name}'")
enriched_channels = []
for channel in channels:
enriched = provider.enrich_channel_data(channel, **kwargs)
if enriched is not None:
enriched_channels.append(enriched)
logger.info(f"ProviderManager: Enriched {len(enriched_channels)} out of {len(channels)} channels")
return enriched_channels
return channels
def get_channel_manifest(self, provider_name: str, channel_id: str, **kwargs) -> Optional[str]:
"""
Get manifest URL for a specific channel from a provider.
Args:
provider_name: Name of the provider
channel_id: ID of the channel
**kwargs: Additional arguments (e.g., country)
Returns:
Manifest URL string, or None if not available
Raises:
ValueError: If provider not found
"""
provider = self.get_provider(provider_name)
if not provider:
logger.error(f"ProviderManager: Cannot get manifest - provider '{provider_name}' not found")
raise ValueError(f"Provider '{provider_name}' not found")
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
logger.debug(f"ProviderManager: Retrieved manifest for channel '{channel_id}' from provider '{provider_name}'")
else:
logger.warning(f"ProviderManager: No manifest available for channel '{channel_id}' from provider '{provider_name}'")
return manifest_url
def get_channel_epg(self, provider_name: str, channel_id: str, **kwargs) -> List[Dict]:
"""
Get EPG data for a specific channel from a provider.
Args:
provider_name: Name of the provider
channel_id: ID of the channel
**kwargs: Additional arguments (e.g., start_time, end_time, country)
Returns:
List of EPG entries
Raises:
ValueError: If provider not found
"""
provider = self.get_provider(provider_name)
if not provider:
logger.error(f"ProviderManager: Cannot get EPG - provider '{provider_name}' not found")
raise ValueError(f"Provider '{provider_name}' not found")
# CHECK IF PROVIDER IMPLEMENTS ITS OWN EPG
if provider.implements_epg:
# Use provider's native EPG implementation
logger.debug(f"ProviderManager: Using native EPG for provider '{provider_name}'")
epg_data = provider.get_epg(channel_id, **kwargs)
else:
# Use generic EPG manager
logger.debug(f"ProviderManager: Using generic EPG for provider '{provider_name}'")
epg_data = self.epg_manager.get_epg(
provider_name=provider_name,
channel_id=channel_id,
start_time=kwargs.get('start_time'),
end_time=kwargs.get('end_time')
)
logger.debug(
f"ProviderManager: Retrieved {len(epg_data)} EPG entries for channel '{channel_id}' from provider '{provider_name}'")
return epg_data
def get_provider_epg_xmltv(self, provider_name: str, **kwargs) -> Optional[str]:
"""
Get complete EPG data for a provider in XMLTV format.
Args:
provider_name: Name of the provider
**kwargs: Additional arguments (e.g., country)
Returns:
XMLTV formatted string, or None if not available
Raises:
ValueError: If provider not found
"""
provider = self.get_provider(provider_name)
if not provider:
logger.error(f"ProviderManager: Cannot get XMLTV EPG - provider '{provider_name}' not found")
raise ValueError(f"Provider '{provider_name}' not found")
# Only use provider's XMLTV if it implements EPG
if provider.implements_epg:
logger.debug(f"ProviderManager: Using native XMLTV EPG for provider '{provider_name}'")
xmltv_data = provider.get_epg_xmltv(**kwargs)
else:
# For generic EPG, we don't generate XMLTV format
# (The EPG XML is already in XMLTV format, but it's global, not provider-specific)
logger.warning(
f"ProviderManager: Provider '{provider_name}' does not implement EPG, no provider-specific XMLTV available")
xmltv_data = None
if xmltv_data:
logger.info(f"ProviderManager: Retrieved XMLTV EPG data for provider '{provider_name}'")
else:
logger.warning(f"ProviderManager: No XMLTV EPG data available for provider '{provider_name}'")
return xmltv_data
def clear_epg_cache(self) -> bool:
"""
Clear the generic EPG cache.
Returns:
True if cleared successfully
"""
logger.info("ProviderManager: Clearing EPG cache")
return self.epg_manager.clear_cache()
def reload_epg_mapping(self) -> bool:
"""
Reload EPG channel mapping from file.
Returns:
True if reloaded successfully
"""
logger.info("ProviderManager: Reloading EPG mapping")
return self.epg_manager.reload_mapping()
def get_epg_cache_info(self) -> Optional[Dict]:
"""
Get information about EPG cache.
Returns:
Dictionary with cache info, or None if no cache
"""
return self.epg_manager.get_cache_info()
def get_epg_mapping_stats(self) -> Dict:
"""
Get statistics about EPG channel mapping.
Returns:
Dictionary with mapping statistics
"""
return self.epg_manager.get_mapping_stats()
def has_epg_mapping(self, provider_name: str, channel_id: str) -> bool:
"""
Check if EPG mapping exists for a specific channel.
Args:
provider_name: Name of provider
channel_id: Channel ID
Returns:
True if mapping exists
"""
return self.epg_manager.has_mapping_for_channel(provider_name, channel_id)
def get_channel_drm_configs(self, provider_name: str, channel_id: str, **kwargs) -> List:
provider = self.get_provider(provider_name)
if not provider:
logger.error(f"ProviderManager: Cannot get DRM configs - provider '{provider_name}' not found")
raise ValueError(f"Provider '{provider_name}' not found")
logger.debug(f"ProviderManager: Getting DRM configs for channel '{channel_id}' from provider '{provider_name}'")
# Get raw DRM configs from provider
drm_configs = provider.get_drm(channel_id, **kwargs)
logger.debug(f"ProviderManager: Retrieved {len(drm_configs)} raw DRM configs")
# Check if we need PSSH data at all
pssh_data_list = []
if drm_configs and self.drm_plugin_manager.plugins:
# Get DRM systems from configs
config_drm_systems = {config.system for config in drm_configs}
# Get DRM systems that have plugins (excluding GENERIC which processes all)
plugin_drm_systems = set(self.drm_plugin_manager.plugins.keys())
# Check if there's overlap OR if there's a GENERIC plugin (which processes everything)
needs_pssh = bool(
config_drm_systems & plugin_drm_systems or
DRMSystem.GENERIC in plugin_drm_systems
)
if needs_pssh:
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
logger.debug(
f"ProviderManager: PSSH needed - extracting from manifest (matching systems: {config_drm_systems & plugin_drm_systems})")
try:
pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
logger.debug(f"ProviderManager: Extracted {len(pssh_data_list)} PSSH data entries")
except Exception as e:
logger.warning(f"ProviderManager: Could not extract PSSH data from manifest: {e}")
else:
logger.debug(
f"ProviderManager: No matching plugins for DRM systems {config_drm_systems}, skipping PSSH extraction")
else:
logger.debug(f"ProviderManager: No configs or no plugins registered, skipping PSSH extraction")
# Process through DRM plugins with PSSH data
processed_configs = self.drm_plugin_manager.process_drm_configs(drm_configs, pssh_data_list, **kwargs)
logger.info(
f"ProviderManager: Processed DRM configs for channel '{channel_id}' - {len(processed_configs)} configs returned")
return processed_configs
def _extract_pssh_from_manifest(self, manifest_url: str) -> List:
"""
Extract PSSH data from a manifest URL.
Args:
manifest_url: URL of the manifest to parse
Returns:
List of PSSHData objects extracted from the manifest
"""
import requests
from .utils.manifest_parser import ManifestParser
try:
# Fetch the manifest content
response = requests.get(manifest_url, timeout=10)
response.raise_for_status()
# Parse and extract PSSH data
return ManifestParser.extract_pssh_from_manifest(response.text, manifest_url)
except Exception as e:
logger.warning(f"ProviderManager: Failed to fetch or parse manifest from {manifest_url}: {e}")
return []
def get_all_channels(self, fetch_manifests: bool = True, **kwargs) -> Dict[str, List[StreamingChannel]]:
"""
Get channels from all registered providers.
Args:
fetch_manifests: Whether to enrich channels with manifest data
**kwargs: Additional arguments for channel fetching
Returns:
Dictionary mapping provider names to their channels
"""
logger.info(f"ProviderManager: Fetching channels from all {len(self.providers)} providers (fetch_manifests={fetch_manifests})")
result = {}
total_channels = 0
for name in self.providers:
try:
channels = self.get_channels(name, fetch_manifests, **kwargs)
result[name] = channels
total_channels += len(channels)
except Exception as e:
logger.error(f"ProviderManager: Failed to get channels from provider '{name}': {e}")
result[name] = []
logger.info(f"ProviderManager: Retrieved {total_channels} total channels from all providers")
return result
def list_providers(self) -> List[str]:
"""List names of all registered providers"""
return list(self.providers.keys())
def clear_providers(self) -> None:
"""Clear all registered providers"""
provider_count = len(self.providers)
self.providers.clear()
logger.info(f"ProviderManager: Cleared {provider_count} registered providers")
def list_drm_plugins(self) -> Dict:
"""List all registered DRM plugins"""
return self.drm_plugin_manager.list_plugins()
def clear_drm_plugins(self) -> None:
"""Clear all registered DRM plugins"""
logger.info("ProviderManager: Clearing all DRM plugins")
self.drm_plugin_manager.clear_plugins()