From 346d3ae38c0cc2c30ff0cc5c5010b79056a50aa1 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Sat, 21 Feb 2026 17:07:58 +0100 Subject: [PATCH] Add discovery --- .../providers/discovery/auth.py | 62 ++++++++++++++++--- .../providers/discovery/constants.py | 27 ++++++-- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/lib/streaming_providers/providers/discovery/auth.py b/lib/streaming_providers/providers/discovery/auth.py index 35b64f7..dabda3a 100644 --- a/lib/streaming_providers/providers/discovery/auth.py +++ b/lib/streaming_providers/providers/discovery/auth.py @@ -35,7 +35,9 @@ from .constants import ( DISCOVERY_DEVICE_INFO_TEMPLATE, DISCOVERY_DISCO_CLIENT, DISCOVERY_DISCO_PARAMS, - DISCOVERY_GISDK_CLIENT_ID, + DISCOVERY_FEATURE_FLAGS_PAYLOAD, + DISCOVERY_FEATURE_FLAGS_URL, + DISCOVERY_HMAC_KEY, DISCOVERY_USER_AGENT, HOME_MARKET_MAPPING, AuthProvider, @@ -268,6 +270,9 @@ class DiscoveryAuthenticator(BaseAuthenticator): self._disco_id: Optional[str] = None self._wbd_ace: Optional[str] = None + # GI SDK client ID — fetched from feature flags, session-specific + self._gisdk_client_id: Optional[str] = None + # Store http_manager and proxy_config BEFORE calling super().__init__ self._http_manager = http_manager self._proxy_config = proxy_config @@ -527,13 +532,48 @@ class DiscoveryAuthenticator(BaseAuthenticator): else "https://default.any-any.prd.api.discoveryplus.com/cms/collections" ) + def _fetch_feature_flags(self) -> None: + """ + Fetch feature flags to obtain a session-specific GI SDK client ID. + + The x-gisdk clientId in x-gisdk header is session-specific and comes + from the feature flags decisions endpoint. Also validates HMAC keys + and Arkose config are as expected. + + Populates self._gisdk_client_id. + """ + try: + response = self.http_manager.post( + DISCOVERY_FEATURE_FLAGS_URL, + operation="auth", + headers={ + "User-Agent": DISCOVERY_USER_AGENT, + "Content-Type": "application/json", + }, + json_data=DISCOVERY_FEATURE_FLAGS_PAYLOAD, + ) + response.raise_for_status() + data = response.json() + + # Extract gisdk clientId from oauth config (session-specific) + oauth_cfg = data.get("oauth", {}).get("config", {}) + client_id = oauth_cfg.get("webToMobileAuth", {}).get("clientId") + if client_id: + self._gisdk_client_id = client_id + logger.debug(f"Fetched GI SDK client ID from feature flags: {client_id}") + else: + logger.warning("No GI SDK client ID in feature flags response") + + except Exception as e: + logger.warning(f"Feature flags fetch failed, using fallback gisdk clientId: {e}") + def _build_client_id(self) -> str: """ Build x-disco-client-id header value. - Format: web1_{env}:{timestamp}:{hmac_sha256} + Format: web1_prd:{timestamp}:{hmac_sha256} The HMAC-SHA256 is computed over "{prefix}:{timestamp}" using the - GI SDK client ID as the key. + decoded HMAC key from the feature flags hmacKeys.web.key field. Returns: Client ID string @@ -544,7 +584,7 @@ class DiscoveryAuthenticator(BaseAuthenticator): timestamp = str(int(time.time())) message = f"{DISCOVERY_CLIENT_ID_PREFIX}:{timestamp}" signature = hmac_lib.new( - DISCOVERY_GISDK_CLIENT_ID.encode("utf-8"), + DISCOVERY_HMAC_KEY.encode("utf-8"), message.encode("utf-8"), hashlib.sha256, ).hexdigest() @@ -575,7 +615,7 @@ class DiscoveryAuthenticator(BaseAuthenticator): "x-disco-client": DISCOVERY_DISCO_CLIENT, "x-disco-client-id": self._build_client_id(), "x-disco-params": DISCOVERY_DISCO_PARAMS, - "x-gisdk": f"clientId={DISCOVERY_GISDK_CLIENT_ID}", + "x-gisdk": f"clientId={self._gisdk_client_id}" if self._gisdk_client_id else None, "x-wbd-device-consent": DISCOVERY_DEVICE_CONSENT, "x-wbd-preferred-language": f"{self.country.lower()}-DE", "x-wbd-time-zone": DISCOVERY_DEFAULT_TIMEZONE, @@ -589,7 +629,8 @@ class DiscoveryAuthenticator(BaseAuthenticator): if self._wbd_ace: headers["x-wbd-ace"] = self._wbd_ace - return headers + # Remove any None values (e.g. x-gisdk before feature flags are fetched) + return {k: v for k, v in headers.items() if v is not None} def _get_auth_headers(self) -> Dict[str, str]: """ @@ -715,18 +756,21 @@ class DiscoveryAuthenticator(BaseAuthenticator): f"Performing two-step anonymous authentication for {self.provider_name}" ) + # Fetch feature flags first — provides session-specific GI SDK client ID + self._fetch_feature_flags() + # Step 1 headers: base headers only, no session headers yet - base_headers = { + base_headers = {k: v for k, v in { "User-Agent": DISCOVERY_USER_AGENT, "x-device-info": self._build_device_info(), "x-disco-client": DISCOVERY_DISCO_CLIENT, "x-disco-client-id": self._build_client_id(), "x-disco-params": DISCOVERY_DISCO_PARAMS, - "x-gisdk": f"clientId={DISCOVERY_GISDK_CLIENT_ID}", + "x-gisdk": f"clientId={self._gisdk_client_id}" if self._gisdk_client_id else None, "x-wbd-device-consent": DISCOVERY_DEVICE_CONSENT, "x-wbd-preferred-language": f"{self.country.lower()}-DE", "x-wbd-time-zone": DISCOVERY_DEFAULT_TIMEZONE, - } + }.items() if v is not None} params = {"realm": "bolt"} diff --git a/lib/streaming_providers/providers/discovery/constants.py b/lib/streaming_providers/providers/discovery/constants.py index 9320a6b..7b20a78 100644 --- a/lib/streaming_providers/providers/discovery/constants.py +++ b/lib/streaming_providers/providers/discovery/constants.py @@ -142,8 +142,8 @@ DEFAULT_DEVICE_ID: Final[str] = "a2f463fa-2052-4af1-ae16-26d8289c6b94" # 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 -DISCOVERY_DISCO_CLIENT: Final[str] = f"WEB:x86_64:dplus:{DISCOVERY_CLIENT_VERSION}" +# 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" @@ -183,11 +183,26 @@ DISCOVERY_ARKOSE_FC_URL: Final[str] = ( # HMAC / Client ID Configuration # ============================================================================ -# GI SDK client ID — used as the HMAC-SHA256 key for x-disco-client-id -DISCOVERY_GISDK_CLIENT_ID: Final[str] = "9f964812-8935-4293-a135-81be80f14c77" +# 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: web1_{env} -DISCOVERY_CLIENT_ID_PREFIX: Final[str] = f"web1_{DEFAULT_ENV}" +# 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]: