diff --git a/lib/streaming_providers/providers/discovery/channel_manager.py b/lib/streaming_providers/providers/discovery/channel_manager.py index eeee868..e257bc4 100644 --- a/lib/streaming_providers/providers/discovery/channel_manager.py +++ b/lib/streaming_providers/providers/discovery/channel_manager.py @@ -24,7 +24,7 @@ class DiscoveryChannelManager: Responsibilities: - Fetching and parsing distribution channels from the CMS /home route - - Maintaining the ``_channels_cache`` (channel_id → DiscoveryChannel) + - Maintaining the ``_channels_cache`` (edit_id → DiscoveryChannel) - Discovering and caching navigation routes from the /home response graph into ``_cms_routes`` (route_id → label) @@ -163,6 +163,27 @@ class DiscoveryChannelManager: except Exception as e: logger.warning(f"Could not discover CMS routes at init: {e}") + def get_by_distribution_id( + self, distribution_id: str + ) -> Optional[DiscoveryChannel]: + """ + Look up a cached DiscoveryChannel by its CMS distribution UUID. + + The cache is keyed by ``edit_id``, so this performs a linear scan + using the ``distribution_id`` stored in ``raw_data``. The channel + list is small (typically < 50), so this is acceptable. + + Args: + distribution_id: The distributionChannel UUID from the CMS. + + Returns: + Matching DiscoveryChannel, or None if not found. + """ + for channel in self._channels_cache.values(): + if channel.raw_data.get("distribution_id") == distribution_id: + return channel + return None + # ========================================================================= # CMS route discovery # ========================================================================= diff --git a/lib/streaming_providers/providers/discovery/event_manager.py b/lib/streaming_providers/providers/discovery/event_manager.py index a3c7bc1..a88aef4 100644 --- a/lib/streaming_providers/providers/discovery/event_manager.py +++ b/lib/streaming_providers/providers/discovery/event_manager.py @@ -278,17 +278,24 @@ class DiscoveryEventManager: relationships, now_utc, start_dt, end_dt ) - # ---- channel lookup ----------------------------------------- - channel_name: Optional[str] = None + # ---- edit_id from distributionChannel cache ----------------- + # The distributionChannel relationship carries the CMS UUID + # (distribution_id). The cache is keyed by edit_id, so we + # use the reverse map on the provider to resolve it. + edit_id: Optional[str] = None dist_channel_ref = ( relationships.get("distributionChannel", {}).get("data", {}) ) dist_channel_id = ( dist_channel_ref.get("id") if dist_channel_ref else None ) + channel_name: Optional[str] = None if dist_channel_id: - cached = self._channels_cache.get(dist_channel_id) + cached = self._provider.channel_manager.get_by_distribution_id( + dist_channel_id + ) if cached: + edit_id = cached.channel_id # channel_id == edit_id channel_name = cached.name else: logger.debug( @@ -299,13 +306,13 @@ class DiscoveryEventManager: # ---- build Event -------------------------------------------- event = Event( name=attributes.get("name", "Unknown Event"), - content_id=airing.get("id", ""), + content_id=edit_id or airing.get("id", ""), provider=self._provider.provider_name, logo_url=None, mode="live" if status == EventStatus.LIVE else "vod", - session_manifest=False, - manifest_script=None, - cdm=None, + session_manifest=True if edit_id else False, + manifest_script=edit_id, + cdm=edit_id, content_type="AIRING", description=attributes.get("description", ""), genre=None, @@ -613,7 +620,7 @@ class DiscoveryEventManager: event = Event( name=attributes.get("name", "Unknown Event"), - content_id=video_data.get("id", ""), + content_id=edit_id or video_data.get("id", ""), provider=self._provider.provider_name, logo_url=logo_url, mode=( @@ -621,11 +628,9 @@ class DiscoveryEventManager: if attributes.get("videoType") == "LIVE" else "vod" ), - session_manifest=True, - manifest_script=( - f"editid={edit_id}" if edit_id else None - ), - cdm=f"editid={edit_id}" if edit_id else None, + session_manifest=True if edit_id else False, + manifest_script=edit_id, + cdm=edit_id, content_type="EVENT", description=attributes.get("description", ""), genre=genre, diff --git a/lib/streaming_providers/providers/discovery/models.py b/lib/streaming_providers/providers/discovery/models.py index 784afdd..6959014 100644 --- a/lib/streaming_providers/providers/discovery/models.py +++ b/lib/streaming_providers/providers/discovery/models.py @@ -109,24 +109,30 @@ class DiscoveryChannel: attributes = distribution_data.get("attributes", {}) relationships = distribution_data.get("relationships", {}) - # Get edit ID + # edit_id is the playback identifier — used as content_id throughout edit_data = relationships.get("edit", {}).get("data", {}) edit_id = edit_data.get("id") + # distribution_id is the CMS channel UUID — kept as metadata only + distribution_id = distribution_data.get("id", "") + # Extract logo logo_url = cls._extract_logo_url(relationships, included_by_id) + raw = distribution_data.copy() + raw["distribution_id"] = distribution_id + return cls( name=attributes.get("name", "Unknown Channel"), - channel_id=distribution_data.get("id", ""), + channel_id=edit_id or distribution_id, # edit_id is the primary key edit_id=edit_id, logo_url=logo_url, description=attributes.get("description", ""), mode=StreamingMode.LIVE.value, session_manifest=True, - manifest_script=f"editid={edit_id}" if edit_id else None, - cdm=f"editid={edit_id}" if edit_id else None, - raw_data=distribution_data.copy(), + manifest_script=distribution_id or None, + cdm=distribution_id or None, + raw_data=raw, ) @classmethod @@ -150,23 +156,26 @@ class DiscoveryChannel: attributes = api_data.get("attributes", {}) relationships = api_data.get("relationships", {}) - # Get edit ID (for VOD, the ID itself is often the edit_id) + # For VOD/events, the item ID itself is the edit_id (playback identifier) edit_id = api_data.get("id") # Extract logo logo_url = cls._extract_logo_url(relationships, included_by_id) + raw = api_data.copy() + raw["source_id"] = edit_id # preserve original CMS ID as metadata + channel = cls( name=attributes.get("name", "Unknown Event"), - channel_id=api_data.get("id", ""), + channel_id=edit_id or "", # edit_id is the primary key edit_id=edit_id, logo_url=logo_url, content_type="VOD", mode=StreamingMode.VOD.value, session_manifest=True, - manifest_script=f"editid={edit_id}" if edit_id else None, - cdm=f"editid={edit_id}" if edit_id else None, - raw_data=api_data.copy(), + manifest_script=edit_id or None, + cdm=edit_id or None, + raw_data=raw, **kwargs ) @@ -314,13 +323,9 @@ class DiscoveryChannel: # Transfer raw_data channel.raw_data = self.raw_data.copy() - # Store edit_id and timestamps in raw_data for later use - if self.edit_id: - channel.raw_data["edit_id"] = self.edit_id - if self.start_time: - channel.raw_data["start_time"] = self.start_time - if self.end_time: - channel.raw_data["end_time"] = self.end_time + # Store distribution_id in raw_data for reference (channels only) + if "distribution_id" in self.raw_data: + channel.raw_data["distribution_id"] = self.raw_data["distribution_id"] return channel diff --git a/lib/streaming_providers/providers/discovery/playback_manager.py b/lib/streaming_providers/providers/discovery/playback_manager.py index 0bfbfc1..cc1bdda 100644 --- a/lib/streaming_providers/providers/discovery/playback_manager.py +++ b/lib/streaming_providers/providers/discovery/playback_manager.py @@ -21,7 +21,6 @@ from ...base.utils.logger import logger from .constants import PlatformOS, get_default_capabilities, get_default_device_info, get_drm_request_headers from .exceptions import ManifestFetchError, PlaybackRestrictedException -from .models import DiscoveryChannel class DiscoveryPlaybackManager: @@ -43,11 +42,9 @@ class DiscoveryPlaybackManager: def __init__( self, provider, # DiscoveryProvider — avoid circular import - channels_cache: Dict[str, DiscoveryChannel], playback_cache: Dict[str, tuple], ): self._provider = provider - self._channels_cache = channels_cache # {edit_id: (expiry_timestamp, playback_data)} self._playback_cache = playback_cache @@ -63,9 +60,9 @@ class DiscoveryPlaybackManager: """ Populate streaming data for a list of StreamingChannel objects. - Looks up each channel's ``edit_id`` in the shared ``_channels_cache``, - fetches (or returns cached) playback info, and attaches the manifest - URL, streaming format, and DRM config to the channel object. + ``channel.channel_id`` is the ``edit_id`` (playback identifier), so + it is passed directly to ``get_cached_playback_info`` — no cache + indirection required. Args: channels: StreamingChannel objects to populate. @@ -83,17 +80,10 @@ class DiscoveryPlaybackManager: while retries < max_retries and not success and not is_restricted: try: - disco_channel = self._channels_cache.get(channel.channel_id) - if not disco_channel: - logger.warning( - f"Channel {channel.name} not in cache, skipping" - ) - break - - edit_id = disco_channel.edit_id + edit_id = channel.channel_id if not edit_id: logger.warning( - f"No edit_id for channel {channel.name}, skipping" + f"No edit_id (channel_id) for {channel.name}, skipping" ) break @@ -120,11 +110,6 @@ class DiscoveryPlaybackManager: channel.license_url = streaming_data["license_url"] channel.cdm_type = streaming_data["drm_system"] - if streaming_data["drm_auth"]: - disco_channel.raw_data["drm_auth"] = ( - streaming_data["drm_auth"] - ) - logger.info( f"Streaming data populated for: {channel.name}" ) @@ -319,7 +304,7 @@ class DiscoveryPlaybackManager: "applicationSessionId": str(uuid.uuid4()), "userPreferences": { "videoQuality": "best", - "uiLanguage": f"{self._provider.country}-DE".upper(), + "uiLanguage": f"{self._provider.country.lower()}-{self._provider.country.upper()}", }, "features": ["mlp"], } diff --git a/lib/streaming_providers/providers/discovery/provider.py b/lib/streaming_providers/providers/discovery/provider.py index fc6b722..bbc83bb 100644 --- a/lib/streaming_providers/providers/discovery/provider.py +++ b/lib/streaming_providers/providers/discovery/provider.py @@ -35,7 +35,7 @@ from .constants import ( get_user_agent, ) from .event_manager import DiscoveryEventManager -from .exceptions import ChannelNotFoundError, ManifestFetchError +from .exceptions import ManifestFetchError from .models import DiscoveryChannel from .playback_manager import DiscoveryPlaybackManager @@ -116,7 +116,7 @@ class DiscoveryProvider(StreamingProvider): # ------------------------------------------------------------------ # Shared caches (owned here; passed by reference into each manager) # ------------------------------------------------------------------ - # DiscoveryChannel objects keyed by channel_id + # DiscoveryChannel objects keyed by edit_id (the playback identifier) self._channels_cache: Dict[str, DiscoveryChannel] = {} # Navigation routes discovered from /home: { route_id: label } self._cms_routes: Dict[str, str] = {} @@ -236,7 +236,6 @@ class DiscoveryProvider(StreamingProvider): ) self.playback_manager = DiscoveryPlaybackManager( provider=self, - channels_cache=self._channels_cache, playback_cache=self._playback_cache, ) @@ -408,70 +407,48 @@ class DiscoveryProvider(StreamingProvider): def get_manifest(self, content_id: str, **kwargs) -> Optional[str]: """ - Get manifest URL for a channel by ID. + Get manifest URL for a channel or event by ID. - Raises: - ChannelNotFoundError: If channel not in cache. + ``content_id`` is the ``edit_id`` (playback identifier), so it is + passed directly to the playback manager — no cache indirection needed. """ try: - disco_channel = self._channels_cache.get(content_id) - if not disco_channel: - raise ChannelNotFoundError(content_id) - - edit_id = disco_channel.edit_id - if not edit_id: - raise ManifestFetchError( - f"No edit_id for channel {content_id}" - ) - playback_data = self.playback_manager.get_cached_playback_info( - edit_id=edit_id + edit_id=content_id ) streaming_data = self.playback_manager.extract_streaming_data( playback_data ) return streaming_data.get("manifest_url") - - except (ChannelNotFoundError, ManifestFetchError): + except ManifestFetchError: raise except Exception as e: logger.error( - f"Error getting manifest for channel {content_id}: {e}" + f"Error getting manifest for {content_id}: {e}" ) return None def get_drm(self, content_id: str, **kwargs) -> List[DRMConfig]: - """Get all DRM configurations for a channel by ID.""" + """ + Get all DRM configurations for a channel or event by ID. + + ``content_id`` is the ``edit_id`` (playback identifier), so it is + passed directly to the playback manager — no cache indirection needed. + """ try: - disco_channel = self._channels_cache.get(content_id) - if not disco_channel: - logger.warning( - f"Channel {content_id} not in cache for DRM" - ) - return [] - - edit_id = disco_channel.edit_id - if not edit_id: - return [] - playback_data = self.playback_manager.get_cached_playback_info( - edit_id=edit_id + edit_id=content_id ) streaming_data = self.playback_manager.extract_streaming_data( playback_data ) - if not streaming_data["license_url"]: return [] - - drm_config = self.playback_manager.build_drm_config( - streaming_data - ) + drm_config = self.playback_manager.build_drm_config(streaming_data) return [drm_config] if drm_config else [] - except Exception as e: logger.error( - f"Error getting DRM configs for channel {content_id}: {e}" + f"Error getting DRM configs for {content_id}: {e}" ) return []