From d8c030650ba1168428f06a70dee375bd6bd28a92 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Wed, 4 Mar 2026 14:52:17 +0100 Subject: [PATCH] Add discovery --- .../providers/discovery/constants.py | 20 ++++ .../providers/discovery/playback_manager.py | 110 +++++++++++++++++- 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/lib/streaming_providers/providers/discovery/constants.py b/lib/streaming_providers/providers/discovery/constants.py index 99ee749..6937c74 100644 --- a/lib/streaming_providers/providers/discovery/constants.py +++ b/lib/streaming_providers/providers/discovery/constants.py @@ -499,6 +499,26 @@ DEFAULT_MAX_RETRIES: Final[int] = 3 DEFAULT_RETRY_DELAY: Final[int] = 1 +# ============================================================================ +# CDN Configuration +# ============================================================================ + +# CDN prefixes extracted from the subdomain of manifest URLs. +# Example URL: https://cf.dplus.eu.prd.media.max.com/protected +# ^^ ← CDN prefix is "cf" +# +# If the resolved manifest URL's CDN prefix is in this list, the playback +# request will be retried (up to CDN_MAX_RETRIES times) in the hope that +# the next request lands on an acceptable CDN. +BLACKLISTED_CDN_PREFIXES: Final[List[str]] = [ + "akm", # Akamai — blacklisted; prefer Cloudfront ("cf") or others +] + +# Maximum number of extra playback attempts when a blacklisted CDN is encountered. +# After this many retries the blacklisted URL is accepted as a last resort. +CDN_MAX_RETRIES: Final[int] = 5 + + # ============================================================================ # Error Codes # ============================================================================ diff --git a/lib/streaming_providers/providers/discovery/playback_manager.py b/lib/streaming_providers/providers/discovery/playback_manager.py index cc1bdda..3323ccc 100644 --- a/lib/streaming_providers/providers/discovery/playback_manager.py +++ b/lib/streaming_providers/providers/discovery/playback_manager.py @@ -19,7 +19,14 @@ from typing import Any, Dict, List, Optional from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel from ...base.utils.logger import logger -from .constants import PlatformOS, get_default_capabilities, get_default_device_info, get_drm_request_headers +from .constants import ( + BLACKLISTED_CDN_PREFIXES, + CDN_MAX_RETRIES, + PlatformOS, + get_default_capabilities, + get_default_device_info, + get_drm_request_headers, +) from .exceptions import ManifestFetchError, PlaybackRestrictedException @@ -92,7 +99,7 @@ class DiscoveryPlaybackManager: f"(edit_id: {edit_id}, attempt {retries + 1})" ) - playback_data = self.get_cached_playback_info( + playback_data = self.get_playback_info_with_cdn_check( edit_id=edit_id ) streaming_data = self.extract_streaming_data(playback_data) @@ -231,7 +238,18 @@ class DiscoveryPlaybackManager: ) data = self.get_playback_info(edit_id=edit_id) + self._store_playback_cache(edit_id, data) + return data + def _store_playback_cache(self, edit_id: str, data: Dict) -> None: + """ + Compute the expiry timestamp from playback data and write it to the cache. + + Args: + edit_id: Content edit ID used as the cache key. + data: Playback API response dict. + """ + now = time.time() expiry = None try: drm_expiration_str = data.get("drm", {}).get("expirationDate") @@ -255,7 +273,6 @@ class DiscoveryPlaybackManager: f"Playback cache stored for edit_id {edit_id} " f"(expires in {int(expiry - now)}s)" ) - return data # ========================================================================= # Payload / streaming data helpers @@ -384,6 +401,93 @@ class DiscoveryPlaybackManager: return result + # ========================================================================= + # CDN helpers + # ========================================================================= + + @staticmethod + def _extract_cdn_prefix(url: str) -> Optional[str]: + """ + Extract the CDN prefix from a manifest URL. + + The prefix is the first label of the hostname. + Example: ``https://cf.dplus.eu.prd.media.max.com/...`` → ``"cf"`` + + Args: + url: Manifest URL string. + + Returns: + Lowercase CDN prefix, or None if the URL cannot be parsed. + """ + try: + import urllib.parse + hostname = urllib.parse.urlparse(url).hostname or "" + prefix = hostname.split(".")[0] + return prefix.lower() if prefix else None + except Exception: + return None + + @staticmethod + def _is_blacklisted_cdn(url: str) -> bool: + """ + Return True if the manifest URL's CDN prefix is blacklisted. + + Args: + url: Manifest URL string. + + Returns: + True when the CDN prefix appears in BLACKLISTED_CDN_PREFIXES. + """ + prefix = DiscoveryPlaybackManager._extract_cdn_prefix(url) + return prefix in BLACKLISTED_CDN_PREFIXES + + def get_playback_info_with_cdn_check(self, edit_id: str) -> Dict: + """ + Fetch playback info, retrying up to CDN_MAX_RETRIES times if the + resolved manifest URL lands on a blacklisted CDN. + + Each retry issues a completely fresh playback request (bypassing the + cache) so Discovery+ may route us to a different CDN node. After all + retries are exhausted the last response is returned regardless of CDN. + + Args: + edit_id: Content edit ID. + + Returns: + Playback info dictionary — CDN-filtered when possible. + """ + cdn_prefix='' + playback_data = {} + + for attempt in range(1, CDN_MAX_RETRIES + 1): + # Always fetch fresh — we need a new request to get a new CDN assignment. + playback_data = self.get_playback_info(edit_id=edit_id) + + manifest_url = playback_data.get("manifest", {}).get("url", "") + cdn_prefix = self._extract_cdn_prefix(manifest_url) + + if not self._is_blacklisted_cdn(manifest_url): + if attempt > 1: + logger.info( + f"CDN check passed on attempt {attempt}/{CDN_MAX_RETRIES} " + f"(cdn={cdn_prefix}, edit_id={edit_id})" + ) + self._store_playback_cache(edit_id, playback_data) + return playback_data + + logger.warning( + f"Blacklisted CDN '{cdn_prefix}' on attempt {attempt}/{CDN_MAX_RETRIES} " + f"for edit_id={edit_id} — retrying" + ) + + # All retries exhausted — accept whatever CDN we got last. + logger.warning( + f"All {CDN_MAX_RETRIES} CDN retries exhausted for edit_id={edit_id}; " + f"accepting blacklisted CDN '{cdn_prefix}'" + ) + self._store_playback_cache(edit_id, playback_data) + return playback_data + def build_drm_config( self, streaming_data: Dict[str, Any] ) -> Optional[DRMConfig]: