Add discovery

This commit is contained in:
Nirvana
2026-03-03 09:55:39 +01:00
parent c984f80a88
commit 6cc50616ff
10 changed files with 782 additions and 443 deletions
@@ -263,19 +263,19 @@ class BHTelecomProvider(StreamingProvider):
def get_manifest(
self,
channel_id: str,
content_id: str,
**kwargs,
) -> Optional[str]:
# Hits the cache logically
channels = self.get_channels(fetch_manifests=False)
for channel in channels:
if channel.channel_id == channel_id:
if channel.channel_id == content_id:
return channel.manifest
return None
def get_drm(
self,
channel_id: str,
content_id: str,
**kwargs,
) -> List[DRMConfig]:
return []
@@ -125,6 +125,14 @@ CMS_INCLUDE_PARAMS: Final[str] = "default"
CMS_PAGE_SIZE: Final[int] = 50
CMS_SCHEDULE_INCLUDE_PARAMS: Final[str] = "items.video.items.video.badges,items.video.primaryChannel,items.video.images,items.video.edit"
# CMS route identifiers
# /sports — the primary sports landing page; returns airing items directly in
# the top-level `included` array (no nested collection hop required).
# /sport-schedule — legacy schedule route; uses the deeper collection traversal
# path and is kept as a fallback.
CMS_ROUTE_SPORTS: Final[str] = "sports"
CMS_ROUTE_SPORT_SCHEDULE: Final[str] = "sport-schedule"
# Known collection IDs for TV channels (will be discovered dynamically, but these are fallbacks)
CHANNEL_COLLECTIONS: Final[Dict[str, List[str]]] = {
"de": [
@@ -142,6 +150,26 @@ CHANNEL_COLLECTIONS: Final[Dict[str, List[str]]] = {
# Collection item types that represent channels
CHANNEL_ITEM_TYPES: Final[List[str]] = ["distributionChannel", "channel", "linearChannel", "liveChannel"]
# ============================================================================
# Airing / Event Badge Constants
# ============================================================================
# Badge IDs returned on `airing` items in the CMS `/sports` route response.
# These map directly to EventStatus values in the provider.
#
# "live" → EventStatus.LIVE (event is currently airing)
# "release-state-up-next" → EventStatus.UPCOMING (event is scheduled soon)
#
# Note: The legacy event path used "release-state-upcoming" as the up-next badge
# ID. The /sports airing items use "release-state-up-next" (with a trailing
# "-next"). Both are handled in get_events() for backward compatibility.
AIRING_BADGE_LIVE: Final[str] = "live"
AIRING_BADGE_UP_NEXT: Final[str] = "release-state-up-next"
AIRING_BADGE_UPCOMING_LEGACY: Final[str] = "release-state-upcoming"
# Type string for airing items returned on the /sports route
AIRING_ITEM_TYPE: Final[str] = "airing"
# ============================================================================
# User Agent Configuration
# ============================================================================
File diff suppressed because it is too large Load Diff
@@ -337,7 +337,7 @@ class HRTiProvider(StreamingProvider):
logger.error(f"Traceback: {traceback.format_exc()}")
return channel
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
def get_manifest(self, content_id: str, **kwargs) -> Optional[str]:
"""
Get manifest URL for a channel by authorizing a session
"""
@@ -345,8 +345,8 @@ class HRTiProvider(StreamingProvider):
# Authorize session for this channel
session_data = self.authenticator.authorize_session(
content_type="tlive", # TV live
content_ref_id=channel_id,
channel_id=channel_id,
content_ref_id=content_id,
channel_id=content_id,
)
if session_data and session_data.get("Authorized", False):
@@ -354,17 +354,17 @@ class HRTiProvider(StreamingProvider):
# The actual manifest will be resolved during playback with session authorization
channels = self.get_channels()
for channel in channels:
if channel.channel_id == channel_id:
if channel.channel_id == content_id:
return channel.manifest_script # This is the streaming URL
logger.warning(f"Session authorization failed for channel {channel_id}")
logger.warning(f"Session authorization failed for channel {content_id}")
return None
except Exception as e:
logger.error(f"Error getting manifest for channel {channel_id}: {e}")
logger.error(f"Error getting manifest for channel {content_id}: {e}")
return None
def get_drm(self, channel_id: str, session_data: Dict = None, **kwargs) -> List[DRMConfig]:
def get_drm(self, content_id: str, session_data: Dict = None, **kwargs) -> List[DRMConfig]:
"""
Get DRM configurations for a channel with proper license data.
If session_data is not provided, will authorize a new session.
@@ -373,7 +373,7 @@ class HRTiProvider(StreamingProvider):
# If no session data provided, authorize a new session
if not session_data:
logger.debug(
f"No session data provided for DRM - authorizing new session for channel {channel_id}"
f"No session data provided for DRM - authorizing new session for channel {content_id}"
)
# Find the channel to get content type and streaming URL
@@ -384,12 +384,12 @@ class HRTiProvider(StreamingProvider):
)
target_channel = None
for ch in channels:
if ch.channel_id == channel_id:
if ch.channel_id == content_id:
target_channel = ch
break
if not target_channel:
logger.error(f"Channel {channel_id} not found for DRM authorization")
logger.error(f"Channel {content_id} not found for DRM authorization")
return []
# Determine content type
@@ -407,35 +407,35 @@ class HRTiProvider(StreamingProvider):
content_drm_id = f"{path_parts[0]}_{path_parts[1]}"
logger.debug(
f"Authorizing session for DRM - channel: {channel_id}, content_type: {content_type}, drm_id: {content_drm_id}"
f"Authorizing session for DRM - channel: {content_id}, content_type: {content_type}, drm_id: {content_drm_id}"
)
# Authorize session
session_data = self.authenticator.authorize_session(
content_type=content_type,
content_ref_id=channel_id,
content_ref_id=content_id,
content_drm_id=content_drm_id,
video_store_ids=None,
channel_id=channel_id,
channel_id=content_id,
start_time=None,
end_time=None,
)
if not session_data:
logger.error(f"Failed to authorize session for DRM - channel {channel_id}")
logger.error(f"Failed to authorize session for DRM - channel {content_id}")
return []
# Check if authorized
if not session_data.get("Authorized", False):
logger.warning(f"Session not authorized for DRM - channel {channel_id}")
logger.warning(f"Session not authorized for DRM - channel {content_id}")
return []
logger.debug(f"Session authorized for DRM - channel {channel_id}")
logger.debug(f"Session authorized for DRM - channel {content_id}")
# Report session event (use full SessionId, not DrmId)
session_id = session_data.get("SessionId")
if session_id:
self.authenticator.report_session_event(session_id, channel_id)
self.authenticator.report_session_event(session_id, content_id)
# IMPORTANT: For license data, use DrmId (not SessionId)
# DrmId is the short random string for DRM
@@ -473,7 +473,7 @@ class HRTiProvider(StreamingProvider):
# Create the license configuration
# Use the class method to create LicenseConfig with base64 encoded req_data
license_config = LicenseConfig.create_with_base64_req_data(
license_config = LicenseConfig.create_with_req_data(
req_data_template="{CHA-RAW}", # The placeholder string
server_url=self.hrti_config.license_url,
use_http_get_request=False,
@@ -486,11 +486,11 @@ class HRTiProvider(StreamingProvider):
# Create the DRM configuration
drm_config = DRMConfig(system=DRMSystem.WIDEVINE, priority=1, license=license_config)
logger.debug(f"Created DRM config for channel {channel_id} with DrmId {drm_id}")
logger.debug(f"Created DRM config for channel {content_id} with DrmId {drm_id}")
return [drm_config]
except Exception as e:
logger.error(f"Error getting DRM config for channel {channel_id}: {e}")
logger.error(f"Error getting DRM config for channel {content_id}: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
@@ -574,7 +574,7 @@ class JoynProvider(StreamingProvider):
def get_manifest(
self,
channel_id: str,
content_id: str,
content_type: str = CONTENT_TYPE_LIVE,
video_config: Optional[Dict] = None,
**kwargs,
@@ -593,20 +593,20 @@ class JoynProvider(StreamingProvider):
"""
try:
entitlement_token = self.get_entitlement_token(
content_id=channel_id, content_type=content_type
content_id=content_id, content_type=content_type
)
playlist_data = self.get_channel_playlist(channel_id, entitlement_token, video_config)
playlist_data = self.get_channel_playlist(content_id, entitlement_token, video_config)
return playlist_data.get("manifestUrl")
except Exception as e:
logger.error(f"Error getting manifest for channel {channel_id}: {e}")
logger.error(f"Error getting manifest for channel {content_id}: {e}")
return None
def get_drm(
self,
channel_id: str,
content_id: str,
content_type: str = CONTENT_TYPE_LIVE,
video_config: Optional[Dict] = None,
**kwargs,
@@ -625,10 +625,10 @@ class JoynProvider(StreamingProvider):
"""
try:
entitlement_token = self.get_entitlement_token(
content_id=channel_id, content_type=content_type
content_id=content_id, content_type=content_type
)
playlist_data = self.get_channel_playlist(channel_id, entitlement_token, video_config)
playlist_data = self.get_channel_playlist(content_id, entitlement_token, video_config)
license_url = playlist_data.get("licenseUrl")
if not license_url:
@@ -655,7 +655,7 @@ class JoynProvider(StreamingProvider):
return [drm_config]
except Exception as e:
logger.error(f"Error getting DRM configs for channel {channel_id}: {e}")
logger.error(f"Error getting DRM configs for channel {content_id}: {e}")
return []
def get_epg(
@@ -1208,7 +1208,7 @@ class M3UProvider(StreamingProvider):
def get_manifest(
self,
channel_id: str,
content_id: str,
**kwargs,
) -> Optional[str]:
"""
@@ -1222,13 +1222,13 @@ class M3UProvider(StreamingProvider):
"""
channels = self.get_channels()
for channel in channels:
if channel.channel_id == channel_id:
if channel.channel_id == content_id:
return channel.manifest
return None
def get_drm(
self,
channel_id: str,
content_id: str,
**kwargs,
) -> List[DRMConfig]:
"""
@@ -1242,7 +1242,7 @@ class M3UProvider(StreamingProvider):
"""
channels = self.get_channels()
for channel in channels:
if channel.channel_id == channel_id:
if channel.channel_id == content_id:
if channel.drm_config is not None:
# Return as a list for consistency
return [channel.drm_config]
@@ -1254,10 +1254,10 @@ class Magenta2Provider(StreamingProvider):
return None
def get_manifest(
self, channel_id: str, content_type: str = CONTENT_TYPE_LIVE, **kwargs
self, content_id: str, content_type: str = CONTENT_TYPE_LIVE, **kwargs
) -> Optional[str]:
"""Get MPD manifest URL using cached SMIL data"""
smil_data = self._get_smil_data(channel_id)
smil_data = self._get_smil_data(content_id)
return smil_data.get("mpd_url") if smil_data else None
def get_catchup_manifest(
@@ -1510,17 +1510,17 @@ class Magenta2Provider(StreamingProvider):
return None
def get_drm(
self, channel_id: str, content_type: str = CONTENT_TYPE_LIVE, **kwargs
self, content_id: str, content_type: str = CONTENT_TYPE_LIVE, **kwargs
) -> List[DRMConfig]:
"""Get DRM configuration using unified SMIL data"""
try:
smil_data = self._get_smil_data(channel_id)
smil_data = self._get_smil_data(content_id)
if not smil_data:
logger.error(f"No SMIL data found for channel {channel_id}")
logger.error(f"No SMIL data found for channel {content_id}")
return []
if not smil_data.get("release_pid"):
logger.error(f"No releasePid found in SMIL for channel {channel_id}")
logger.error(f"No releasePid found in SMIL for channel {content_id}")
# Debug what we do have
logger.debug(f"SMIL data keys: {list(smil_data.keys())}")
if "content" in smil_data and smil_data["content"]:
@@ -1576,12 +1576,12 @@ class Magenta2Provider(StreamingProvider):
)
logger.info(
f"✓ DRM configuration created for channel {channel_id} (releasePid: {release_pid})"
f"✓ DRM configuration created for channel {content_id} (releasePid: {release_pid})"
)
return [drm_config]
except Exception as e:
logger.error(f"Error getting DRM configs for channel {channel_id}: {e}")
logger.error(f"Error getting DRM configs for channel {content_id}: {e}")
return []
def _get_account_uri(self) -> str:
@@ -315,7 +315,7 @@ class MagentaEUProvider(StreamingProvider):
return True
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
def get_manifest(self, content_id: str, **kwargs) -> Optional[str]:
"""Get manifest URL for a channel by ID"""
# Ensure cache is populated
@@ -324,10 +324,10 @@ class MagentaEUProvider(StreamingProvider):
# Look for the channel
for channel in self._channels_cache:
if channel.channel_id == channel_id:
if channel.channel_id == content_id:
return channel.manifest
logger.warning(f"Channel {channel_id} not found in available channels")
logger.warning(f"Channel {content_id} not found in available channels")
return None
def get_catchup_manifest(
@@ -383,24 +383,24 @@ class MagentaEUProvider(StreamingProvider):
logger.warning(f"Falling back to live manifest for channel {channel_id}")
return base_manifest
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
def get_drm(self, content_id: str, **kwargs) -> List[DRMConfig]:
"""Get DRM configurations for channel by ID"""
logger.info(f"=== get_drm_configs_by_id CALLED for channel_id: {channel_id} ===")
logger.info(f"=== get_drm_configs_by_id CALLED for channel_id: {content_id} ===")
# Ensure cache is populated
if not self._ensure_channels_cache():
logger.warning(f"Cannot get DRM for {channel_id}, channels cache unavailable")
logger.warning(f"Cannot get DRM for {content_id}, channels cache unavailable")
return []
# Find channel in cache
channel = None
for cached_channel in self._channels_cache:
if cached_channel.channel_id == channel_id:
if cached_channel.channel_id == content_id:
channel = cached_channel
break
if not channel:
logger.warning(f"Channel with ID {channel_id} not found in cache")
logger.warning(f"Channel with ID {content_id} not found in cache")
return []
# Get DRM config using the existing method
@@ -258,8 +258,8 @@ class RTLPlusProvider(StreamingProvider):
logger.error(f"Error enriching channel data for {channel.name}: {e}")
return channel
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
manifest_url = self.rtl_config.get_manifest_url(channel_id)
def get_manifest(self, content_id: str, **kwargs) -> Optional[str]:
manifest_url = self.rtl_config.get_manifest_url(content_id)
try:
logger.debug(f"RTL+ Manifest Request: GET {manifest_url}")
@@ -334,13 +334,13 @@ class RTLPlusProvider(StreamingProvider):
except Exception:
return manifest_data
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
def get_drm(self, content_id: str, **kwargs) -> List[DRMConfig]:
"""
Get DRM configurations for a channel from RTL+ streaming API
"""
try:
# Fetch manifest data to get license information
manifest_url = self.rtl_config.get_manifest_url(channel_id)
manifest_url = self.rtl_config.get_manifest_url(content_id)
response = self.http_manager.get(manifest_url, operation="manifest")
response.raise_for_status()
@@ -405,10 +405,10 @@ class RTLPlusProvider(StreamingProvider):
return drm_configs
except requests.RequestException as e:
logger.error(f"Error fetching DRM configs for RTL+ channel {channel_id}: {e}")
logger.error(f"Error fetching DRM configs for RTL+ channel {content_id}: {e}")
return []
except Exception as e:
logger.error(f"Error parsing DRM configs for RTL+ channel {channel_id}: {e}")
logger.error(f"Error parsing DRM configs for RTL+ channel {content_id}: {e}")
return []
@staticmethod
@@ -736,7 +736,7 @@ class ScriptsProvider(StreamingProvider):
def get_manifest(
self,
channel_id: str,
content_id: str,
proxy_url: Optional[str] = None,
**kwargs,
) -> Optional[str]:
@@ -755,12 +755,12 @@ class ScriptsProvider(StreamingProvider):
channel = None
channels = self.get_channels()
for ch in channels:
if ch.channel_id == channel_id:
if ch.channel_id == content_id:
channel = ch
break
if not channel:
logger.warning(f"Scripts ({self.provider_label}): Channel not found: {channel_id}")
logger.warning(f"Scripts ({self.provider_label}): Channel not found: {content_id}")
return None
# Parse manifest script parameters
@@ -804,7 +804,7 @@ class ScriptsProvider(StreamingProvider):
def get_drm(
self,
channel_id: str,
content_id: str,
proxy_url: Optional[str] = None,
force_refresh: bool = False,
**kwargs,
@@ -825,20 +825,20 @@ class ScriptsProvider(StreamingProvider):
channel = None
channels = self.get_channels()
for ch in channels:
if ch.channel_id == channel_id:
if ch.channel_id == content_id:
channel = ch
break
if not channel:
logger.warning(f"Scripts ({self.provider_label}): Channel not found: {channel_id}")
logger.warning(f"Scripts ({self.provider_label}): Channel not found: {content_id}")
return []
# Check cache
if not force_refresh and not self._should_refresh_cdm_cache(channel_id):
cached_config = self._cdm_cache.get(channel_id)
if not force_refresh and not self._should_refresh_cdm_cache(content_id):
cached_config = self._cdm_cache.get(content_id)
if cached_config:
self._stats["cache_hits"] += 1
logger.debug(f"Scripts ({self.provider_label}): Returning DRM config from cache for {channel_id}")
logger.debug(f"Scripts ({self.provider_label}): Returning DRM config from cache for {content_id}")
return [cached_config]
self._stats["cache_misses"] += 1
@@ -866,12 +866,12 @@ class ScriptsProvider(StreamingProvider):
return []
# Parse DRM config
drm_config = self._parse_cdm_output(output, channel_id)
drm_config = self._parse_cdm_output(output, content_id)
if drm_config:
# Update cache
self._cdm_cache[channel_id] = drm_config
self._cdm_cache_timestamps[channel_id] = time.time()
self._cdm_cache[content_id] = drm_config
self._cdm_cache_timestamps[content_id] = time.time()
return [drm_config]
return []