# streaming_providers/providers/discovery/constants.py """ Discovery+ Provider Constants All configuration values for the Discovery+ streaming provider. """ from enum import Enum from typing import Dict, List, Final, Optional # ============================================================================ # Enums for Type Safety # ============================================================================ class StreamingMode(str, Enum): """Streaming modes""" LIVE = "live" VOD = "vod" class CDMMode(str, Enum): """CDM (Content Decryption Module) modes""" EXTERNAL = "external" INTERNAL = "internal" class VideoQuality(str, Enum): """Video quality settings""" BEST = "best" HD = "hd" SD = "sd" class DRMSystem(str, Enum): """DRM systems""" WIDEVINE = "widevine" CLEARKEY = "clearkey" PLAYREADY = "playready" class AuthProvider(str, Enum): """Authentication providers""" USERNAME_PASSWORD = "USERNAME_PASSWORD" ANONYMOUS = "anonymous" class PlatformOS(str, Enum): """ Target OS platform used for device/header spoofing. LINUX → Chrome browser, Widevine + ClearKey DRM, x-disco-client os_version="0.0.0" WINDOWS → Microsoft Edge browser, PlayReady DRM, x-disco-client os_version="NT 10.0" """ LINUX = "linux" WINDOWS = "windows" # Active platform — change this single constant to switch all OS-dependent behaviour. # LINUX preserves existing behaviour exactly; WINDOWS mirrors a real Edge/Windows client. DEFAULT_PLATFORM_OS: Final[PlatformOS] = PlatformOS.WINDOWS # ============================================================================ # Basic Configuration # ============================================================================ # Provider information DISCOVERY_LOGO: Final[ str] = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Discovery%2B.png/960px-Discovery%2B.png" # Bootstrap endpoint for dynamic configuration DISCOVERY_BOOTSTRAP_URL: Final[ str] = "https://default.any-any.prd.api.discoveryplus.com/session-context/headwaiter/v1/bootstrap" # User profile endpoint — returns currentLocationTerritory for country detection DISCOVERY_USERS_ME_URL: Final[ str] = "https://default.dplus-emea.prd.api.discoveryplus.com/users/me" # Default realm DEFAULT_REALM: Final[str] = "bolt" # ============================================================================ # Authentication Configuration # ============================================================================ # Supported auth types SUPPORTED_AUTH_TYPES: Final[List[str]] = [ "anonymous", # Anonymous token "user_credentials", # Username/password login ] # ============================================================================ # Country/Region Configuration # ============================================================================ # Wildcard sentinel — Discovery+ accepts any country; the user's actual country # is determined at runtime via the /users/me endpoint (currentLocationTerritory). SUPPORTED_COUNTRIES: Final[List[str]] = ["*"] # Home market mapping (immutable) HOME_MARKET_MAPPING: Final[Dict[str, str]] = { "de": "emea", "at": "emea", "ch": "emea", "dk": "emea", "fi": "emea", "no": "emea", "se": "emea", "it": "emea", "nl": "emea", "es": "emea", "uk": "uk", "ie": "uk", } # Tenant DEFAULT_TENANT: Final[str] = "dplus" # Environment DEFAULT_ENV: Final[str] = "prd" # Domain DEFAULT_DOMAIN: Final[str] = "api.discoveryplus.com" # Default country DEFAULT_COUNTRY: Final[str] = "de" # ============================================================================ # CMS Configuration # ============================================================================ 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": [ "158509151486067887066538918357725817419", # "Unsere TV-Kanäle" (Germany) "191019300316436018732917730141471355952", # DMAX Austria ], "at": [ "191019300316436018732917730141471355952", # DMAX Austria ], "ch": [], "uk": [], "ie": [], } # 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 # ============================================================================ # Per-platform user agent strings. # LINUX → Chrome on Linux (original behaviour) # WINDOWS → Edge on Windows (matches real captured HAR traffic) DISCOVERY_USER_AGENT_LINUX: Final[str] = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36" ) DISCOVERY_USER_AGENT_WINDOWS: Final[str] = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0" ) # Convenience alias — resolves to the Linux UA by default so existing callers # that reference DISCOVERY_USER_AGENT directly are unaffected. DISCOVERY_USER_AGENT: Final[str] = DISCOVERY_USER_AGENT_LINUX # ============================================================================ # Device Configuration # ============================================================================ # Device ID (consistent across sessions for anonymous tracking) DEFAULT_DEVICE_ID: Final[str] = "a2f463fa-2052-4af1-ae16-26d8289c6b94" # ============================================================================ # Client / Header Configuration # ============================================================================ # Client version string — used in x-disco-client and x-device-info headers DISCOVERY_CLIENT_VERSION: Final[str] = "6.14.0" # Full x-disco-client header value — WEB:{os_version}:dplus:{client_version} # OS version is "0.0.0" for web platform # x-disco-params header value DISCOVERY_DISCO_PARAMS: Final[str] = "realm=bolt,bid=dplus,features=ar" # x-device-info header templates — one per OS platform. # Format placeholders: {device_id}, {session_id} DISCOVERY_DEVICE_INFO_TEMPLATE_LINUX: Final[str] = ( f"dplus/{DISCOVERY_CLIENT_VERSION} (desktop/desktop; Linux/x86_64; {{device_id}}/{{session_id}})" ) DISCOVERY_DEVICE_INFO_TEMPLATE_WINDOWS: Final[str] = ( f"dplus/{DISCOVERY_CLIENT_VERSION} (desktop/desktop; Windows/NT 10.0; {{device_id}}/{{session_id}})" ) # Backward-compatible alias — resolves to Linux template (original behaviour) DISCOVERY_DEVICE_INFO_TEMPLATE: Final[str] = DISCOVERY_DEVICE_INFO_TEMPLATE_LINUX # x-wbd-device-consent header value DISCOVERY_DEVICE_CONSENT: Final[str] = "gpc=0" # Default timezone for x-wbd-time-zone header DISCOVERY_DEFAULT_TIMEZONE: Final[str] = "Europe/Berlin" # Origin / Referer for auth requests (login, bootstrap) DISCOVERY_AUTH_ORIGIN: Final[str] = "https://auth.discoveryplus.com" DISCOVERY_AUTH_REFERER: Final[str] = "https://auth.discoveryplus.com/" # Arkose (FunCaptcha) — required before POST /login DISCOVERY_ARKOSE_SITEKEY: Final[str] = "09CB1E22-0FB7-4AD8-957C-4F06CAFB3C7E" DISCOVERY_ARKOSE_DATA_URL: Final[str] = ( "https://default.dplus-emea.prd.api.discoveryplus.com/users/arkose/data" ) DISCOVERY_ARKOSE_DATA_PAYLOAD: Final[Dict] = { "data": { "attributes": {"relativePath": "/login", "styleTheme": "dplus"}, "type": "arkoseDataExchange", } } # Arkose FunCaptcha token exchange — Discovery+ hosts their own FC endpoint DISCOVERY_ARKOSE_FC_URL: Final[str] = ( f"https://a4gds3vfh.discoveryplus.com/fc/gt2/public_key/{DISCOVERY_ARKOSE_SITEKEY}" ) # ============================================================================ # HMAC / Client ID Configuration # ============================================================================ # HMAC key for web platform (base64-decoded from feature flags response) # Raw b64: NTVlZWExODktZTliNi00NzlmLWJjNTEtMjIyNGNmZGE1NmZl DISCOVERY_HMAC_KEY: Final[str] = "55eea189-e9b6-479f-bc51-2224cfda56fe" # x-disco-client-id prefix matches hmacKeys "id" field for web DISCOVERY_CLIENT_ID_PREFIX: Final[str] = "web1_prd" # x-disco-client header — WEB:{os_version}:dplus:{client_version} # os_version encodes the OS: "0.0.0" for Linux web, "NT 10.0" for Windows DISCOVERY_DISCO_CLIENT_LINUX: Final[str] = f"WEB:0.0.0:dplus:{DISCOVERY_CLIENT_VERSION}" DISCOVERY_DISCO_CLIENT_WINDOWS: Final[str] = f"WEB:NT 10.0:dplus:{DISCOVERY_CLIENT_VERSION}" # Backward-compatible alias — resolves to Linux value (original behaviour) DISCOVERY_DISCO_CLIENT: Final[str] = DISCOVERY_DISCO_CLIENT_LINUX # Feature flags endpoint — provides hmacKeys, gisdk clientId, arkose config etc. # x-gisdk clientId comes from the response and is session-specific. DISCOVERY_FEATURE_FLAGS_URL: Final[str] = ( "https://default.any-any.prd.api.discoveryplus.com/labs/api/v1/sessions/feature-flags/decisions" ) DISCOVERY_FEATURE_FLAGS_PAYLOAD: Final[Dict] = { "context": {"deviceType": "desktop", "domain": "discoveryplus.com"}, "projectId": "7e52f0d0-d8b5-4eda-983b-597e4e2102a2", } def get_default_device_info( platform_os: Optional[PlatformOS] = None, device_id: Optional[str] = None, ) -> Dict[str, any]: """ Return a fresh copy of default device info for the given OS platform. Args: platform_os: Target OS platform. Defaults to DEFAULT_PLATFORM_OS. PlatformOS.LINUX → Chrome on Linux (original behaviour) PlatformOS.WINDOWS → Edge on Windows device_id: Device ID to embed. Defaults to DEFAULT_DEVICE_ID. Pass the authenticator's self.device_id so the value matches what the server tracks per session. Returns: Dictionary containing device information suitable for the playbackInfo body. """ if platform_os is None: platform_os = DEFAULT_PLATFORM_OS if device_id is None: device_id = DEFAULT_DEVICE_ID if platform_os == PlatformOS.WINDOWS: os_info = {"name": "Windows", "version": "NT 10.0"} browser_info = {"name": "Microsoft Edge", "version": "139.0.0.0"} else: # LINUX — preserves original behaviour exactly os_info = {"name": "Linux", "version": "0.0.0"} browser_info = {"name": "Chrome", "version": "144.0.0.0"} return { "deviceId": device_id, "browser": browser_info, "make": "desktop", "model": "desktop", "os": os_info, "platform": "WEB", "deviceType": "web", "player": { "sdk": {"name": "Beam Player Desktop", "version": DISCOVERY_CLIENT_VERSION}, "mediaEngine": { "name": "GLUON_BROWSER", "version": "7.0.0", "labsConfigVersion": "playback_engine_gluon@web-1.2.0,playback_engine_gluon_disable_network_request_event@default-1.0.0,playback_engine_gluon_segment_download_during_drminit@1.0.0,playback_engine_gluon_abr_protocol_hybrid@1.0.0,playback_engine_gluon_initial_track_selection@1.0.0", }, "playerView": {"height": 1080, "width": 1920}, }, } def get_default_capabilities(platform_os: Optional[PlatformOS] = None) -> Dict[str, any]: """ Return a fresh copy of default capabilities for the given OS platform. The DRM advertised differs by platform: LINUX → ClearKey + Widevine L3 (original behaviour) WINDOWS → ClearKey + PlayReady SL3000 (matches real Edge/Windows client) Args: platform_os: Target OS platform. Defaults to DEFAULT_PLATFORM_OS. Returns: Dictionary containing device capabilities suitable for the playbackInfo body. """ if platform_os is None: platform_os = DEFAULT_PLATFORM_OS if platform_os == PlatformOS.WINDOWS: cdm_list = [ {"drmKeySystem": "clearkey"}, {"drmKeySystem": "playready", "maxSecurityLevel": "sl3000"}, ] else: # LINUX — preserves original behaviour exactly cdm_list = [ {"drmKeySystem": "clearkey"}, {"drmKeySystem": "widevine", "maxSecurityLevel": "l3"}, ] return { "manifests": {"formats": {"dash": {}}}, "codecs": { "audio": {"decoders": [{"codec": "aac", "profiles": ["lc", "hev", "hev2"]}]}, "video": { "decoders": [ { "codec": "h264", "profiles": ["high", "main", "baseline"], "maxLevel": "5.2", "levelConstraints": { "width": {"min": 0, "max": 3840}, "height": {"min": 0, "max": 2160}, "framerate": {"min": 0, "max": 60}, }, }, { "codec": "h265", "profiles": ["main10", "main"], "maxLevel": "5.2", "levelConstraints": { "width": {"min": 0, "max": 3840}, "height": {"min": 0, "max": 2160}, "framerate": {"min": 0, "max": 60}, }, }, ], "hdrFormats": [], }, }, "contentProtection": { "contentDecryptionModules": cdm_list }, "devicePlatform": { "memory": {"allocatedMemory": 0, "freeAvailableMemory": 1.7976931348623157e308}, "network": { "capabilities": {"protocols": {"http": {"byteRangeRequests": True}}}, "lastKnownStatus": {"networkTransportType": "unknown"}, }, "videoSink": { "capabilities": {"colorGamuts": ["standard"], "hdrFormats": []}, "lastKnownStatus": {"height": 1080, "width": 1920}, }, }, } def get_disco_client(platform_os: Optional[PlatformOS] = None) -> str: """ Return the x-disco-client header value for the given OS platform. Args: platform_os: Target OS platform. Defaults to DEFAULT_PLATFORM_OS. Returns: x-disco-client header string, e.g. "WEB:0.0.0:dplus:6.14.0" """ if platform_os is None: platform_os = DEFAULT_PLATFORM_OS if platform_os == PlatformOS.WINDOWS: return DISCOVERY_DISCO_CLIENT_WINDOWS return DISCOVERY_DISCO_CLIENT_LINUX def get_device_info_template(platform_os: Optional[PlatformOS] = None) -> str: """ Return the x-device-info header template for the given OS platform. The caller must still call .format(device_id=..., session_id=...) on the result. Args: platform_os: Target OS platform. Defaults to DEFAULT_PLATFORM_OS. Returns: x-device-info template string with {device_id} and {session_id} placeholders. """ if platform_os is None: platform_os = DEFAULT_PLATFORM_OS if platform_os == PlatformOS.WINDOWS: return DISCOVERY_DEVICE_INFO_TEMPLATE_WINDOWS return DISCOVERY_DEVICE_INFO_TEMPLATE_LINUX # ============================================================================ # Playback Configuration # ============================================================================ DEFAULT_PLAYBACK_SESSION_ID: Final[str] = "fb20e22e-abcd-490a-a83d-8c71f79e7435" DEFAULT_APPLICATION_SESSION_ID: Final[str] = "74985592-a061-4597-ab5b-51492d0fc2c6" # ============================================================================ # DRM Configuration # ============================================================================ DRM_SYSTEM_WIDEVINE: Final[str] = DRMSystem.WIDEVINE.value DRM_SYSTEM_PLAYREADY: Final[str] = DRMSystem.PLAYREADY.value def get_user_agent(platform_os: Optional[PlatformOS] = None) -> str: """ Return the User-Agent string for the given OS platform. Args: platform_os: Target OS platform. Defaults to DEFAULT_PLATFORM_OS. Returns: User-Agent header string. """ if platform_os is None: platform_os = DEFAULT_PLATFORM_OS if platform_os == PlatformOS.WINDOWS: return DISCOVERY_USER_AGENT_WINDOWS return DISCOVERY_USER_AGENT_LINUX def get_drm_request_headers(platform_os: Optional[PlatformOS] = None) -> Dict[str, str]: """ Return a fresh copy of DRM request headers for the given OS platform. Args: platform_os: Target OS platform. Defaults to DEFAULT_PLATFORM_OS. Returns: Dictionary containing DRM request headers. """ return { "Content-Type": "application/octet-stream", "User-Agent": get_user_agent(platform_os), "Origin": "https://play.discoveryplus.com", "Referer": "https://play.discoveryplus.com/", "traceparent": "00-aba275155d9ed5c2b54d576682cb30e6-ddfd854be17b3a6e-01", } # ============================================================================ # Request Configuration # ============================================================================ DEFAULT_REQUEST_TIMEOUT: Final[int] = 30 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 # ============================================================================ class ErrorCode(str, Enum): """Discovery+ error codes""" PLAYBACK_RESTRICTED = "PLAYBACK_RESTRICTED" UNAUTHORIZED = "UNAUTHORIZED" NOT_FOUND = "NOT_FOUND" GEOBLOCKED = "GEOBLOCKED" INVALID_CREDENTIALS = "INVALID_CREDENTIALS" AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"