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

242 lines
7.8 KiB
Python

#!/usr/bin/env python3
# streaming_providers/base/epg/epg_manager.py
"""
EPG Manager - Central coordinator for EPG operations
Orchestrates cache, mapping, and parsing components
"""
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from .epg_cache import EPGCache
from .epg_mapping import EPGMapping
from .epg_parser import EPGParser
from ..utils.logger import logger
class EPGManager:
"""
Central manager for EPG operations.
Coordinates cache management, channel mapping, and EPG parsing.
All file operations are handled transparently by VFS - works in both
Kodi and standard Python environments.
"""
# Default EPG source URL (can be overridden by addon setting)
DEFAULT_EPG_URL = "https://example.com/epg.xml.gz"
def __init__(self, epg_url: Optional[str] = None):
"""
Initialize EPG manager with all components.
Args:
epg_url: EPG source URL (defaults to setting or DEFAULT_EPG_URL)
"""
# Initialize components - they handle paths internally
self.cache = EPGCache(vfs_subdir="epg_cache")
self.mapping = EPGMapping() # No paths needed - handles internally
self.parser = EPGParser()
# Get EPG URL from settings or use default
self.epg_url = epg_url or self._get_epg_url_from_settings() or self.DEFAULT_EPG_URL
logger.info(f"EPGManager: Initialized with EPG URL: {self.epg_url}")
@staticmethod
def _get_epg_url_from_settings() -> Optional[str]:
"""
Get EPG URL from addon settings using SettingsManager.
Returns:
URL from settings, or None if not set
"""
try:
from ..settings.kodi_settings_bridge import KodiSettingsBridge
bridge = KodiSettingsBridge()
if bridge.is_kodi_environment():
url = bridge.addon.getSetting('epg_xml_url')
if url and url.strip():
logger.info(f"EPGManager: Using EPG URL from settings: {url}")
return url.strip()
except Exception as e:
logger.debug(f"EPGManager: Could not read EPG URL setting: {e}")
return None
def _get_default_time_range(self) -> tuple:
"""
Get default time range for EPG queries (now to +12 hours).
Returns:
Tuple of (start_time, end_time) as Unix timestamps
"""
now = datetime.now()
start = int(now.timestamp())
end = int((now + timedelta(hours=12)).timestamp())
logger.debug(f"EPGManager: Using default time range: {start} to {end}")
return start, end
def get_epg(
self,
provider_name: str,
channel_id: str,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None
) -> List[Dict]:
"""
Get EPG data for a specific channel within a time range.
This is the main entry point for EPG queries.
Args:
provider_name: Name of provider (e.g., "rtlplus", "joyn_de")
channel_id: Channel ID within provider
start_time: Start of time range (datetime), None for now
end_time: End of time range (datetime), None for now+12h
Returns:
List of EPG entries as dictionaries (empty on any error)
"""
logger.info(f"EPGManager: Getting EPG for {provider_name}/{channel_id}")
try:
# Step 1: Map to EPG channel ID
epg_channel_id = self.mapping.get_epg_channel_id(provider_name, channel_id)
if not epg_channel_id:
logger.warning(
f"EPGManager: No EPG mapping found for {provider_name}/{channel_id}"
)
return []
logger.debug(f"EPGManager: Mapped to EPG channel ID: {epg_channel_id}")
# Step 2: Get or download EPG XML
xml_path = self.cache.get_or_download(self.epg_url)
if not xml_path:
logger.error("EPGManager: Failed to get EPG XML file")
return []
# Step 3: Convert datetime to Unix timestamps
if start_time is None or end_time is None:
default_start, default_end = self._get_default_time_range()
start_ts = int(start_time.timestamp()) if start_time else default_start
end_ts = int(end_time.timestamp()) if end_time else default_end
else:
start_ts = int(start_time.timestamp())
end_ts = int(end_time.timestamp())
logger.debug(f"EPGManager: Time range: {start_ts} to {end_ts}")
# Step 4: Parse EPG for channel and time range
epg_entries = self.parser.parse_epg_for_channel(
xml_path,
epg_channel_id,
start_ts,
end_ts
)
logger.info(
f"EPGManager: Retrieved {len(epg_entries)} EPG entries for "
f"{provider_name}/{channel_id}"
)
return epg_entries
except Exception as e:
logger.error(f"EPGManager: Error getting EPG: {e}", exc_info=True)
return []
def get_epg_for_provider(
self,
provider_name: str,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None
) -> Dict[str, List[Dict]]:
"""
Get EPG data for all channels of a provider.
Args:
provider_name: Name of provider
start_time: Start of time range (datetime)
end_time: End of time range (datetime)
Returns:
Dictionary mapping channel IDs to their EPG entries
"""
logger.info(f"EPGManager: Getting EPG for all channels of provider '{provider_name}'")
result = {}
# Get all mapped channels for this provider
provider_mapping = self.mapping.get_provider_mapping(provider_name)
if not provider_mapping:
logger.warning(f"EPGManager: No channels mapped for provider '{provider_name}'")
return result
# Get EPG for each channel
for channel_id in provider_mapping.keys():
epg_entries = self.get_epg(provider_name, channel_id, start_time, end_time)
if epg_entries:
result[channel_id] = epg_entries
logger.info(
f"EPGManager: Retrieved EPG for {len(result)} channels of provider '{provider_name}'"
)
return result
def clear_cache(self) -> bool:
"""
Clear EPG cache.
Returns:
True if cleared successfully
"""
logger.info("EPGManager: Clearing EPG cache")
return self.cache.clear_cache()
def reload_mapping(self) -> bool:
"""
Reload channel mapping from file.
Returns:
True if reloaded successfully
"""
logger.info("EPGManager: Reloading channel mapping")
return self.mapping.reload_mapping()
def get_cache_info(self) -> Optional[Dict]:
"""
Get information about EPG cache.
Returns:
Dictionary with cache info, or None if no cache
"""
return self.cache.get_cache_info()
def get_mapping_stats(self) -> Dict:
"""
Get statistics about channel mapping.
Returns:
Dictionary with mapping statistics
"""
return self.mapping.get_mapping_stats()
def has_mapping_for_channel(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.mapping.has_mapping(provider_name, channel_id)