Files
script.service.ultimate/lib/streaming_providers/base/drm_operations.py
T

210 lines
7.6 KiB
Python
Raw Normal View History

2025-12-23 16:51:49 +01:00
# ============================================================================
# streaming_providers/base/drm_operations.py
"""
2026-01-16 12:24:41 +01:00
DRM-related operations with caching and optimized PSSH extraction.
2025-12-23 16:51:49 +01:00
"""
2026-01-06 16:41:03 +01:00
2026-01-16 12:24:41 +01:00
import time
from threading import Lock
from typing import Dict, List, Optional, Tuple
2026-01-06 16:41:03 +01:00
2025-12-23 16:51:49 +01:00
from .drm import DRMPluginManager
from .models import DRMSystem
from .utils.logger import logger
2026-01-16 12:24:41 +01:00
class PSSHCache:
"""Thread-safe cache for PSSH data"""
def __init__(self, ttl_seconds: int = 3600):
self.cache: Dict[str, Tuple[List, float]] = {}
self.ttl = ttl_seconds
self.lock = Lock()
def get(self, key: str) -> Optional[List]:
"""Get cached PSSH data if not expired"""
with self.lock:
if key in self.cache:
pssh_list, timestamp = self.cache[key]
if time.time() - timestamp < self.ttl:
logger.debug(f"Cache HIT for {key}")
return pssh_list
else:
logger.debug(f"Cache EXPIRED for {key}")
del self.cache[key]
return None
def set(self, key: str, pssh_list: List):
"""Cache PSSH data"""
with self.lock:
self.cache[key] = (pssh_list, time.time())
logger.debug(f"Cache SET for {key}")
def clear(self):
"""Clear all cache entries"""
with self.lock:
self.cache.clear()
logger.debug("Cache CLEARED")
2026-01-23 14:48:38 +01:00
class DRMConfigCache:
"""Thread-safe cache for DRM configurations (ClearKey only, no expiry)"""
def __init__(self):
self.cache: Dict[str, List] = {}
self.lock = Lock()
def get(self, key: str) -> Optional[List]:
"""Get cached DRM configs"""
with self.lock:
if key in self.cache:
logger.debug(f"DRM Config Cache HIT for {key}")
return self.cache[key]
return None
def set(self, key: str, drm_configs: List):
"""Cache DRM configs (only if contains ClearKey)"""
with self.lock:
self.cache[key] = drm_configs
logger.debug(f"DRM Config Cache SET for {key}")
def clear(self):
"""Clear all cache entries"""
with self.lock:
self.cache.clear()
logger.debug("DRM Config Cache CLEARED")
2025-12-23 16:51:49 +01:00
class DRMOperations:
"""Handles all DRM-related operations."""
2026-01-16 12:24:41 +01:00
def __init__(self, registry, cache_ttl: int = 3600):
2025-12-23 16:51:49 +01:00
self.registry = registry
self.drm_plugin_manager = DRMPluginManager()
2026-01-16 12:24:41 +01:00
self.pssh_cache = PSSHCache(ttl_seconds=cache_ttl)
2026-01-23 14:48:38 +01:00
self.drm_config_cache = DRMConfigCache() # New: ClearKey config cache
2026-01-16 12:24:41 +01:00
logger.debug("DRMOperations: Initialized with caching")
2025-12-23 16:51:49 +01:00
2026-01-16 12:24:41 +01:00
def get_channel_drm_configs(self, provider_name: str, channel_id: str, **kwargs) -> List:
2025-12-23 16:51:49 +01:00
"""Get DRM configurations for a channel."""
2026-01-23 14:48:38 +01:00
cache_key = f"{provider_name}:{channel_id}"
# Check DRM config cache first (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
2025-12-23 16:51:49 +01:00
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
pssh_data_list = []
if drm_configs and self.drm_plugin_manager.plugins:
if self._needs_pssh_extraction(drm_configs):
2026-01-23 14:48:38 +01:00
# Try PSSH cache FIRST before fetching manifest
2026-01-16 18:27:08 +01:00
pssh_data_list = self.pssh_cache.get(cache_key)
if pssh_data_list is None:
# Cache miss - now fetch manifest and extract
2026-01-23 14:48:38 +01:00
logger.debug(f"PSSH cache miss for {cache_key}, fetching manifest")
2026-01-16 18:27:08 +01:00
manifest_url = provider.get_manifest(channel_id, **kwargs)
if manifest_url:
2026-01-16 12:24:41 +01:00
pssh_data_list = self._extract_pssh_from_manifest(manifest_url)
if pssh_data_list:
self.pssh_cache.set(cache_key, pssh_data_list)
2026-01-16 18:27:08 +01:00
else:
logger.debug(f"Using cached PSSH for {cache_key}")
2025-12-23 16:51:49 +01:00
# Process through plugins
processed = self.drm_plugin_manager.process_drm_configs(
drm_configs, pssh_data_list, **kwargs
)
2026-01-23 14:48:38 +01:00
# Cache if contains ClearKey config
if self._has_clearkey_config(processed):
logger.info(f"Caching ClearKey DRM configs for '{channel_id}'")
self.drm_config_cache.set(cache_key, processed)
2025-12-23 16:51:49 +01:00
logger.info(f"Processed DRM for '{channel_id}': {len(processed)} configs")
return processed
2026-01-23 14:48:38 +01:00
@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)
2025-12-23 16:51:49 +01:00
def _needs_pssh_extraction(self, drm_configs) -> bool:
"""Check if PSSH extraction is needed."""
config_systems = {config.system for config in drm_configs}
plugin_systems = set(self.drm_plugin_manager.plugins.keys())
2026-01-16 12:24:41 +01:00
return bool(config_systems & plugin_systems or DRMSystem.GENERIC in plugin_systems)
2025-12-23 16:51:49 +01:00
2026-01-16 12:24:41 +01:00
@staticmethod
def _extract_pssh_from_manifest(manifest_url: str) -> List:
"""Extract PSSH data from manifest with single segment fallback."""
2025-12-23 16:51:49 +01:00
import requests
2026-01-06 16:41:03 +01:00
2025-12-23 16:51:49 +01:00
from .utils.manifest_parser import ManifestParser
try:
response = requests.get(manifest_url, timeout=10)
response.raise_for_status()
2026-01-16 12:24:41 +01:00
manifest_content = response.text
# Extract from manifest content
pssh_list = ManifestParser._extract_from_manifest_content(manifest_content)
# Check if we need segment extraction
needs_segment_extraction = not pssh_list or any(
not p.pssh_box or not p.key_ids for p in pssh_list
2025-12-23 16:51:49 +01:00
)
2026-01-16 12:24:41 +01:00
if needs_segment_extraction:
logger.debug("PSSH incomplete in manifest, extracting from init segment")
# Extract ONE init segment URL
init_segment_url = ManifestParser.extract_single_init_segment_url(
manifest_content, manifest_url
)
if init_segment_url:
# Get expected system IDs
expected_system_ids = [p.system_id for p in pssh_list] if pssh_list else []
segment_pssh = ManifestParser._extract_from_single_segment(
init_segment_url, expected_system_ids
)
if segment_pssh:
# Merge or replace
return ManifestParser._merge_pssh_data(pssh_list, segment_pssh)
else:
logger.warning("Could not extract init segment URL from manifest")
return pssh_list
2025-12-23 16:51:49 +01:00
except Exception as e:
logger.warning(f"Failed to extract PSSH: {e}")
return []
def list_drm_plugins(self) -> Dict:
"""List registered DRM plugins."""
return self.drm_plugin_manager.list_plugins()
def clear_drm_plugins(self):
"""Clear all DRM plugins."""
self.drm_plugin_manager.clear_plugins()
2026-01-16 12:24:41 +01:00
def clear_pssh_cache(self):
"""Clear PSSH cache."""
self.pssh_cache.clear()
2026-01-23 14:48:38 +01:00
def clear_drm_config_cache(self):
"""Clear DRM config cache."""
self.drm_config_cache.clear()