# 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 # ============================================================================ # 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" class AuthProvider(str, Enum): """Authentication providers""" USERNAME_PASSWORD = "USERNAME_PASSWORD" ANONYMOUS = "anonymous" # ============================================================================ # Basic Configuration # ============================================================================ # Provider information DISCOVERY_LOGO: Final[ str] = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Discovery%2B_logo.svg/2560px-Discovery%2B_logo.svg.png" # Bootstrap endpoint for dynamic configuration DISCOVERY_BOOTSTRAP_URL: Final[ str] = "https://default.any-any.prd.api.discoveryplus.com/session-context/headwaiter/v1/bootstrap" # 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 # ============================================================================ # Supported countries (EMEA region) SUPPORTED_COUNTRIES: Final[List[str]] = [ "de", "at", "ch", "dk", "fi", "no", "se", "it", "nl", "es", "uk", "ie", ] # 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 # 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"] # ============================================================================ # User Agent Configuration # ============================================================================ DISCOVERY_USER_AGENT: Final[ str] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" # ============================================================================ # 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 template — format with device_id and session_id DISCOVERY_DEVICE_INFO_TEMPLATE: Final[str] = ( f"dplus/{DISCOVERY_CLIENT_VERSION} (desktop/desktop; Linux/x86_64; {{device_id}}/{{session_id}})" ) # 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 format: WEB:{os_version}:dplus:{client_version} # OS version is "0.0.0" for web (not x86_64 as previously assumed) DISCOVERY_DISCO_CLIENT: Final[str] = f"WEB:0.0.0:dplus:{DISCOVERY_CLIENT_VERSION}" # 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() -> Dict[str, any]: """ Return a fresh copy of default device info. Returns: Dictionary containing device information """ return { "deviceId": DEFAULT_DEVICE_ID, "browser": {"name": "Chrome", "version": "144.0.0.0"}, "make": "desktop", "model": "desktop", "os": {"name": "Linux", "version": "0.0.0"}, "platform": "WEB", "deviceType": "web", "player": { "sdk": {"name": "Beam Player Desktop", "version": "6.14.0"}, "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() -> Dict[str, any]: """ Return a fresh copy of default capabilities. Returns: Dictionary containing device capabilities """ 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": 973}, "height": {"min": 0, "max": 919}, "framerate": {"min": 0, "max": 60}, }, }, { "codec": "h265", "profiles": ["main10", "main"], "maxLevel": "5.2", "levelConstraints": { "width": {"min": 0, "max": 973}, "height": {"min": 0, "max": 919}, "framerate": {"min": 0, "max": 60}, }, }, ], "hdrFormats": [], }, }, "contentProtection": { "contentDecryptionModules": [ {"drmKeySystem": "clearkey"}, {"drmKeySystem": "widevine", "maxSecurityLevel": "l3"}, ] }, "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}, }, }, } # ============================================================================ # 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 def get_drm_request_headers() -> Dict[str, str]: """ Return a fresh copy of DRM request headers. Returns: Dictionary containing DRM request headers """ return { "Content-Type": "application/octet-stream", "User-Agent": DISCOVERY_USER_AGENT, "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 # ============================================================================ # 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"