Files
script.service.ultimate/lib/streaming_providers/providers/discovery/constants.py
T
2026-05-04 20:50:58 +02:00

534 lines
20 KiB
Python

# 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"