This commit is contained in:
Nirvana
2026-01-26 16:32:30 +01:00
parent 8afe07005f
commit 78a39d5601
3 changed files with 366 additions and 67 deletions
@@ -1,4 +1,9 @@
# streaming_providers/base/drm/plugin_manager.py
"""
Manager for DRM configuration plugins with two-phase processing.
Handles plugin registration, discovery, and processing of DRM configs with PSSH data.
"""
import traceback
from typing import Dict, List, Optional
@@ -11,7 +16,9 @@ class DRMPluginManager:
"""
Manager for DRM configuration plugins.
Handles plugin registration, discovery, and processing of DRM configs with PSSH data.
Supports two-phase processing:
- Phase 1: GENERIC plugins (config generators, run before provider)
- Phase 2: System-specific plugins (config transformers, run after provider)
"""
def __init__(self, auto_discover: bool = True):
@@ -51,15 +58,15 @@ class DRMPluginManager:
if drm_system in self.plugins:
existing_plugin = self.plugins[drm_system].plugin_name
logger.warning(
f"DRMPluginManager: Registration failed - DRM system {drm_system} already has plugin '{existing_plugin}' registered"
)
raise ValueError(
f"DRM system {drm_system} already has plugin '{existing_plugin}' registered"
f"DRMPluginManager: Overwriting existing plugin '{existing_plugin}' "
f"with '{plugin_name}' for DRM system {drm_system}"
)
self.plugins[drm_system] = plugin
logger.debug(
f"DRMPluginManager: Successfully registered plugin '{plugin_name}' for DRM system {drm_system}"
phase = "1-GENERIC" if drm_system == DRMSystem.GENERIC else "2-System-specific"
logger.info(
f"DRMPluginManager: Successfully registered plugin '{plugin_name}' "
f"for DRM system {drm_system} (Phase: {phase})"
)
def discover_plugins(self) -> List[str]:
@@ -105,9 +112,9 @@ class DRMPluginManager:
# Only process Python files (not directories or other files)
if (
filename.endswith(".py")
and not filename.startswith("__")
and os.path.isfile(file_path)
filename.endswith(".py")
and not filename.startswith("__")
and os.path.isfile(file_path)
):
scanned_files.append(filename)
@@ -134,9 +141,9 @@ class DRMPluginManager:
for name, obj in inspect.getmembers(module, inspect.isclass):
# Check if it's a DRMPlugin subclass (but not DRMPlugin itself)
if (
issubclass(obj, DRMPlugin)
and obj is not DRMPlugin
and obj.__module__ == module.__name__
issubclass(obj, DRMPlugin)
and obj is not DRMPlugin
and obj.__module__ == module.__name__
):
plugin_classes.append((name, obj))
@@ -193,8 +200,8 @@ class DRMPluginManager:
# Log final discovery results
if registered:
logger.debug(
f"DRMPluginManager: Filesystem autodiscovery completed successfully - {len(registered)} plugins registered: {registered}"
logger.info(
f"DRMPluginManager: Filesystem autodiscovery completed - {len(registered)} plugins registered: {registered}"
)
else:
logger.debug(
@@ -208,13 +215,221 @@ class DRMPluginManager:
return registered
def process_drm_configs(
self, drm_configs: List[DRMConfig], pssh_data_list: List[PSSHData], **kwargs
def has_generic_plugins(self) -> bool:
"""Check if any GENERIC plugins are registered"""
return DRMSystem.GENERIC in self.plugins
def has_system_specific_plugins(self) -> bool:
"""Check if any system-specific (non-GENERIC) plugins are registered"""
return any(sys != DRMSystem.GENERIC for sys in self.plugins.keys())
def process_generic_plugins(
self,
dummy_configs: List[DRMConfig],
pssh_data_list: List[PSSHData],
**kwargs
) -> List[DRMConfig]:
"""
Process a list of DRM configs through registered plugins using PSSH data.
Generic plugins are processed first, and if any ClearKey config is found,
only that config is returned.
PHASE 1: Process through GENERIC plugins only.
GENERIC plugins can CREATE configs from PSSH data.
They receive a dummy config (DRMSystem.NONE) and PSSH data,
and should return valid DRM configs or None.
Args:
dummy_configs: List with dummy config [DRMConfig(system=DRMSystem.NONE)]
pssh_data_list: PSSH data extracted from manifest
**kwargs: Additional context
Returns:
List of generated DRM configs, or empty list if generation failed
"""
if not self.has_generic_plugins():
logger.debug("Phase 1: No GENERIC plugins registered")
return []
if not pssh_data_list:
logger.debug("Phase 1: No PSSH data available for GENERIC plugins")
return []
plugin = self.plugins[DRMSystem.GENERIC]
logger.debug(f"Phase 1: Processing with GENERIC plugin '{plugin.plugin_name}'")
generated_configs = []
try:
# Process each PSSH data entry
for pssh_data in pssh_data_list:
logger.debug(
f"Phase 1: Processing PSSH for system {pssh_data.system_id} "
f"with plugin '{plugin.plugin_name}'"
)
# Pass dummy config - plugin should return real config(s) or None
result = plugin.process_drm_config(
dummy_configs[0], # Dummy config
pssh_data,
**kwargs
)
if result:
generated_configs.append(result)
logger.debug(
f"Phase 1: Plugin '{plugin.plugin_name}' generated "
f"{result.system.value} config"
)
# If ClearKey found, return immediately
if result.system == DRMSystem.CLEARKEY:
logger.info(
f"Phase 1: ClearKey config found from GENERIC plugin, "
f"returning immediately"
)
return [result]
except Exception as e:
logger.error(
f"Phase 1: GENERIC plugin '{plugin.plugin_name}' failed: {e}",
exc_info=True
)
return []
if generated_configs:
logger.info(
f"Phase 1: GENERIC plugin generated {len(generated_configs)} configs"
)
else:
logger.debug("Phase 1: GENERIC plugin generated no configs")
return generated_configs
def process_system_specific_plugins(
self,
drm_configs: List[DRMConfig],
pssh_data_list: List[PSSHData],
**kwargs
) -> List[DRMConfig]:
"""
PHASE 2: Process through system-specific plugins (EXCLUDE GENERIC).
System-specific plugins TRANSFORM existing configs from the provider.
GENERIC plugins are explicitly excluded from this phase.
Args:
drm_configs: DRM configs from the provider
pssh_data_list: PSSH data extracted from manifest
**kwargs: Additional context
Returns:
List of transformed DRM configs
"""
if not drm_configs:
logger.debug("Phase 2: No DRM configs to process")
return []
# Get system-specific plugins only (exclude GENERIC)
system_plugins = {
sys: plugin for sys, plugin in self.plugins.items()
if sys != DRMSystem.GENERIC
}
if not system_plugins:
logger.debug("Phase 2: No system-specific plugins registered, returning provider configs")
return drm_configs
logger.debug(
f"Phase 2: Processing {len(drm_configs)} configs through "
f"{len(system_plugins)} system-specific plugins"
)
# Create a mapping of DRM system to PSSH data for quick lookup
pssh_by_system = {}
for pssh_data in pssh_data_list:
if pssh_data.drm_system:
pssh_by_system[pssh_data.drm_system] = pssh_data
logger.debug(
f"Phase 2: Mapped PSSH data for DRM system: {pssh_data.drm_system}"
)
processed_configs = []
# Process each config through matching plugin
for config in drm_configs:
logger.debug(f"Phase 2: Processing DRM config for system: {config.system}")
if config.system in system_plugins:
plugin = system_plugins[config.system]
pssh_data = pssh_by_system.get(config.system)
if pssh_data:
logger.debug(
f"Phase 2: Using PSSH data for DRM system {config.system}"
)
else:
logger.debug(
f"Phase 2: No PSSH data available for DRM system {config.system}"
)
try:
logger.debug(
f"Phase 2: Processing with plugin '{plugin.plugin_name}'"
)
result = plugin.process_drm_config(config, pssh_data, **kwargs)
if result is not None:
# Check for ClearKey and return immediately if found
if result.system == DRMSystem.CLEARKEY:
logger.info(
f"Phase 2: ClearKey config found from plugin "
f"'{plugin.plugin_name}', returning immediately"
)
return [result]
processed_configs.append(result)
logger.debug(
f"Phase 2: Plugin '{plugin.plugin_name}' successfully "
f"processed config"
)
else:
logger.debug(
f"Phase 2: Plugin '{plugin.plugin_name}' filtered out config "
f"(returned None)"
)
except Exception as e:
logger.warning(
f"Phase 2: Plugin '{plugin.plugin_name}' failed to process "
f"config: {str(e)}"
)
# On error, keep original config
processed_configs.append(config)
logger.debug(f"Phase 2: Using original config as fallback")
else:
# No plugin for this system, keep original
logger.debug(
f"Phase 2: No plugin registered for DRM system {config.system}, "
f"passing through unchanged"
)
processed_configs.append(config)
logger.info(
f"Phase 2: Processed {len(drm_configs)} configs → {len(processed_configs)} configs"
)
return processed_configs
def process_drm_configs(
self,
drm_configs: List[DRMConfig],
pssh_data_list: List[PSSHData],
**kwargs
) -> List[DRMConfig]:
"""
Legacy method: Process configs through all plugins (generic first, then specific).
This maintains backward compatibility but uses single-phase processing.
For new code using two-phase flow, use process_generic_plugins() and
process_system_specific_plugins() separately via DRMOperations.
Args:
drm_configs: List of DRM configs to process
@@ -229,7 +444,8 @@ class DRMPluginManager:
return drm_configs
logger.debug(
f"DRMPluginManager: Processing {len(drm_configs)} DRM configs with {len(pssh_data_list)} PSSH data entries"
f"DRMPluginManager: Processing {len(drm_configs)} DRM configs with "
f"{len(pssh_data_list)} PSSH data entries (legacy single-phase mode)"
)
# Create a mapping of DRM system to PSSH data for quick lookup
@@ -271,8 +487,9 @@ class DRMPluginManager:
# Check for ClearKey and return immediately if found
for config in temp_configs:
if config.system == DRMSystem.CLEARKEY:
logger.debug(
f"DRMPluginManager: ClearKey config found, returning immediately"
logger.info(
f"DRMPluginManager: ClearKey config found from generic plugin, "
f"returning immediately"
)
return [config]
@@ -292,9 +509,10 @@ class DRMPluginManager:
# Find specific plugin for this DRM system
plugin = self.plugins.get(config.system)
if plugin:
if plugin and plugin.supported_drm_system != DRMSystem.GENERIC:
logger.debug(
f"DRMPluginManager: Found plugin '{plugin.plugin_name}' for DRM system {config.system}"
f"DRMPluginManager: Found plugin '{plugin.plugin_name}' for "
f"DRM system {config.system}"
)
try:
@@ -305,36 +523,42 @@ class DRMPluginManager:
)
else:
logger.debug(
f"DRMPluginManager: No PSSH data available for DRM system {config.system}"
f"DRMPluginManager: No PSSH data available for "
f"DRM system {config.system}"
)
processed_config = plugin.process_drm_config(config, pssh_data, **kwargs)
if processed_config is not None:
# Check for ClearKey and return immediately if found
if processed_config.system == DRMSystem.CLEARKEY:
logger.debug(
f"DRMPluginManager: ClearKey config found, returning immediately"
logger.info(
f"DRMPluginManager: ClearKey config found from plugin "
f"'{plugin.plugin_name}', returning immediately"
)
return [processed_config]
final_configs.append(processed_config)
logger.debug(
f"DRMPluginManager: Plugin '{plugin.plugin_name}' successfully processed config"
f"DRMPluginManager: Plugin '{plugin.plugin_name}' successfully "
f"processed config"
)
else:
logger.debug(
f"DRMPluginManager: Plugin '{plugin.plugin_name}' filtered out config (returned None)"
f"DRMPluginManager: Plugin '{plugin.plugin_name}' filtered out "
f"config (returned None)"
)
except Exception as e:
logger.warning(
f"DRMPluginManager: Plugin '{plugin.plugin_name}' failed to process config: {str(e)}"
f"DRMPluginManager: Plugin '{plugin.plugin_name}' failed to process "
f"config: {str(e)}"
)
final_configs.append(config)
logger.debug(f"DRMPluginManager: Using original config as fallback")
else:
logger.debug(
f"DRMPluginManager: No plugin registered for DRM system {config.system}, passing through unchanged"
f"DRMPluginManager: No plugin registered for DRM system {config.system}, "
f"passing through unchanged"
)
final_configs.append(config)
@@ -356,21 +580,27 @@ class DRMPluginManager:
plugin = self.plugins.get(drm_system)
if plugin:
logger.debug(
f"DRMPluginManager: Retrieved plugin '{plugin.plugin_name}' for DRM system {drm_system}"
f"DRMPluginManager: Retrieved plugin '{plugin.plugin_name}' for "
f"DRM system {drm_system}"
)
else:
logger.debug(f"DRMPluginManager: No plugin found for DRM system {drm_system}")
return plugin
def list_plugins(self) -> Dict[DRMSystem, str]:
def list_plugins(self) -> Dict:
"""
List all registered plugins.
List all registered plugins with phase information.
Returns:
Dictionary mapping DRM systems to plugin names
Dictionary mapping DRM system values to plugin info
"""
plugin_list = {
drm_system: plugin.plugin_name for drm_system, plugin in self.plugins.items()
drm_system.value: {
"name": plugin.plugin_name,
"system": drm_system.value,
"phase": "1-GENERIC" if drm_system == DRMSystem.GENERIC else "2-System-specific"
}
for drm_system, plugin in self.plugins.items()
}
logger.debug(f"DRMPluginManager: Currently registered plugins: {plugin_list}")
return plugin_list
@@ -379,4 +609,4 @@ class DRMPluginManager:
"""Clear all registered plugins"""
plugin_count = len(self.plugins)
self.plugins.clear()
logger.debug(f"DRMPluginManager: Cleared {plugin_count} registered plugins")
logger.info(f"DRMPluginManager: Cleared {plugin_count} registered plugins")
+88 -19
View File
@@ -2,6 +2,7 @@
# streaming_providers/base/drm_operations.py
"""
DRM-related operations with caching and optimized PSSH extraction.
Two-phase plugin processing: GENERIC plugins first, then system-specific.
"""
import time
@@ -9,7 +10,7 @@ from threading import Lock
from typing import Dict, List, Optional, Tuple
from .drm import DRMPluginManager
from .models import DRMSystem
from .models import DRMSystem, DRMConfig
from .utils.logger import logger
@@ -82,68 +83,136 @@ class DRMOperations:
self.registry = registry
self.drm_plugin_manager = DRMPluginManager()
self.pssh_cache = PSSHCache(ttl_seconds=cache_ttl)
self.drm_config_cache = DRMConfigCache() # New: ClearKey config cache
logger.debug("DRMOperations: Initialized with caching")
self.drm_config_cache = DRMConfigCache()
logger.debug("DRMOperations: Initialized with two-phase plugin processing")
def get_channel_drm_configs(self, provider_name: str, channel_id: str, **kwargs) -> List:
"""Get DRM configurations for a channel."""
"""
Get DRM configurations for a channel with two-phase plugin processing.
Phase 1: GENERIC plugins (pre-provider) - can generate configs from PSSH
Phase 2: System-specific plugins (post-provider) - transform provider configs
"""
cache_key = f"{provider_name}:{channel_id}"
# Check DRM config cache first (ClearKey configs)
# Step 1: Check DRM config cache (ClearKey configs)
cached_configs = self.drm_config_cache.get(cache_key)
if cached_configs is not None:
logger.info(f"Using cached DRM configs for '{channel_id}'")
return cached_configs
# Cache miss - get from provider
# Step 2: PHASE 1 - Try GENERIC plugins first (if registered)
if DRMSystem.GENERIC in self.drm_plugin_manager.plugins:
logger.debug(f"Phase 1: Attempting GENERIC plugin processing for '{channel_id}'")
generic_configs = self._try_generic_plugins(provider_name, channel_id, cache_key, **kwargs)
if generic_configs: # Plugin successfully generated configs
logger.info(f"Phase 1: GENERIC plugin generated {len(generic_configs)} configs for '{channel_id}'")
# Cache if contains ClearKey
if self._has_clearkey_config(generic_configs):
logger.info(f"Caching ClearKey DRM configs from GENERIC plugin for '{channel_id}'")
self.drm_config_cache.set(cache_key, generic_configs)
return generic_configs
else:
logger.debug(f"Phase 1: GENERIC plugin returned no configs, proceeding to provider")
# Step 3: Get configs from provider (GENERIC plugins didn't provide configs)
provider = self.registry.get_provider(provider_name)
if not provider:
raise ValueError(f"Provider '{provider_name}' not found or disabled")
drm_configs = provider.get_drm(channel_id, **kwargs)
# Extract PSSH if needed
# Step 4: PHASE 2 - Extract PSSH if needed for system-specific plugins
pssh_data_list = []
if drm_configs and self.drm_plugin_manager.plugins:
if drm_configs and self.drm_plugin_manager.has_system_specific_plugins():
if self._needs_pssh_extraction(drm_configs):
# Try PSSH cache FIRST before fetching manifest
# Try PSSH cache first (might have been fetched in Phase 1)
pssh_data_list = self.pssh_cache.get(cache_key)
if pssh_data_list is None:
# Cache miss - now fetch manifest and extract
logger.debug(f"PSSH cache miss for {cache_key}, fetching manifest")
# Cache miss - fetch manifest and extract
logger.debug(f"Phase 2: PSSH cache miss for {cache_key}, fetching manifest")
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
if pssh_data_list:
self.pssh_cache.set(cache_key, pssh_data_list)
else:
logger.debug(f"Using cached PSSH for {cache_key}")
logger.debug(f"Phase 2: Using cached PSSH for {cache_key}")
# Process through plugins
processed = self.drm_plugin_manager.process_drm_configs(
# Step 5: PHASE 2 - Process through system-specific plugins (EXCLUDE GENERIC)
processed = self.drm_plugin_manager.process_system_specific_plugins(
drm_configs, pssh_data_list, **kwargs
)
# Cache if contains ClearKey config
# Step 6: Cache if contains ClearKey config
if self._has_clearkey_config(processed):
logger.info(f"Caching ClearKey DRM configs for '{channel_id}'")
logger.info(f"Caching ClearKey DRM configs from provider for '{channel_id}'")
self.drm_config_cache.set(cache_key, processed)
logger.info(f"Processed DRM for '{channel_id}': {len(processed)} configs")
return processed
def _try_generic_plugins(
self,
provider_name: str,
channel_id: str,
cache_key: str,
**kwargs
) -> Optional[List[DRMConfig]]:
"""
Try to generate configs using GENERIC plugins.
Returns configs if successful, None otherwise.
"""
# Get PSSH data (from cache or manifest)
pssh_data_list = self.pssh_cache.get(cache_key)
if pssh_data_list is None:
logger.debug(f"GENERIC plugin: Fetching PSSH for '{channel_id}'")
provider = self.registry.get_provider(provider_name)
if not provider:
logger.warning(f"Provider '{provider_name}' not found for GENERIC plugin")
return None
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
if pssh_data_list:
self.pssh_cache.set(cache_key, pssh_data_list)
if not pssh_data_list:
logger.debug(f"GENERIC plugin: No PSSH data available for '{channel_id}'")
return None
# Create a dummy config to pass to GENERIC plugin
dummy_configs = [DRMConfig(system=DRMSystem.NONE, priority=0)]
# Let GENERIC plugins try to generate configs
generic_configs = self.drm_plugin_manager.process_generic_plugins(
dummy_configs, pssh_data_list, **kwargs
)
return generic_configs if generic_configs else None
@staticmethod
def _has_clearkey_config(drm_configs: List) -> bool:
"""Check if any config is a ClearKey config"""
return any(config.system == DRMSystem.CLEARKEY for config in drm_configs)
def _needs_pssh_extraction(self, drm_configs) -> bool:
"""Check if PSSH extraction is needed."""
"""Check if PSSH extraction is needed for system-specific plugins."""
config_systems = {config.system for config in drm_configs}
plugin_systems = set(self.drm_plugin_manager.plugins.keys())
# Exclude GENERIC from this check (it's handled separately)
plugin_systems = {
sys for sys in self.drm_plugin_manager.plugins.keys()
if sys != DRMSystem.GENERIC
}
return bool(config_systems & plugin_systems or DRMSystem.GENERIC in plugin_systems)
return bool(config_systems & plugin_systems)
@staticmethod
def _extract_pssh_from_manifest(manifest_url: str) -> List:
@@ -209,18 +209,18 @@ class Sam3Client:
# Store refresh token if provided
if "refresh_token" in data:
new_rt = data["refresh_token"]
logger.debug(f"✅ Refresh token received from {operation}")
logger.debug(f" New refresh_token: {new_rt}")
logger.debug(f" Length: {len(new_rt)} chars")
# logger.debug(f"✅ Refresh token received from {operation}")
# logger.debug(f" New refresh_token: {new_rt}")
# logger.debug(f" Length: {len(new_rt)} chars")
# Compare with old refresh token if we had one
if hasattr(self, 'refresh_token') and self.refresh_token:
if self.refresh_token == new_rt:
logger.debug(f" ℹ️ Same as previous refresh_token")
else:
logger.debug(f" 🔄 Different from previous refresh_token")
logger.debug(f" Old: {self.refresh_token[:30]}...")
logger.debug(f" New: {new_rt[:30]}...")
# if hasattr(self, 'refresh_token') and self.refresh_token:
# if self.refresh_token == new_rt:
# logger.debug(f" ℹ️ Same as previous refresh_token")
# else:
# logger.debug(f" 🔄 Different from previous refresh_token")
# logger.debug(f" Old: {self.refresh_token[:30]}...")
# logger.debug(f" New: {new_rt[:30]}...")
self.refresh_token = new_rt