Add discovery

This commit is contained in:
Nirvana
2026-02-24 18:02:04 +01:00
parent 3dcf03f391
commit 5914d1c505
3 changed files with 79 additions and 5 deletions
@@ -264,16 +264,22 @@ class DiscoveryAuthenticator(BaseAuthenticator):
http_manager=None,
proxy_config: Optional[ProxyConfig] = None,
platform_os: Optional[PlatformOS] = None,
device_id: Optional[str] = None,
):
self.country = country
self.home_market = HOME_MARKET_MAPPING.get(country, "emea")
self.tenant = DEFAULT_TENANT
self.env = DEFAULT_ENV
self.device_id = DEFAULT_DEVICE_ID
# OS platform — controls User-Agent, x-disco-client, x-device-info
self.platform_os: PlatformOS = platform_os if platform_os is not None else DEFAULT_PLATFORM_OS
# Device ID — prefer injected value (from provider via settings_manager),
# fall back to _load_or_create_device_id() for standalone usage.
# Note: set to sentinel now; resolved after super().__init__() below
# so that self.settings_manager is available for the fallback path.
self._injected_device_id = device_id
# Session headers storage (from /token 400 response)
self._session_state: Optional[str] = None
self._disco_id: Optional[str] = None
@@ -311,6 +317,15 @@ class DiscoveryAuthenticator(BaseAuthenticator):
logger.debug(f"Discovery __init__ post-super: self.credentials={type(self.credentials).__name__}")
# Resolve device_id now that super().__init__() has run and
# self.settings_manager (set by BaseAuthenticator) is available.
if self._injected_device_id:
self.device_id = self._injected_device_id
logger.debug(f"Using injected device_id: {self.device_id}")
else:
self.device_id = self._load_or_create_device_id()
del self._injected_device_id # clean up sentinel
# If no credentials were provided at construction time, attempt to load
# from storage. Promote here too — the credential manager always returns
# bare base-class instances.
@@ -355,6 +370,35 @@ class DiscoveryAuthenticator(BaseAuthenticator):
return self._http_manager
def _load_or_create_device_id(self) -> str:
"""
Return a stable device ID for this installation.
Intended to be called after the settings_manager is available.
In normal operation the provider injects the device_id directly via
the constructor, so this is only used as a fallback when the
authenticator is constructed standalone (e.g. in tests).
Falls back to DEFAULT_DEVICE_ID if no settings manager is available.
Returns:
UUID string (with hyphens) to use as device ID
"""
# BaseAuthenticator stores settings_manager as self.settings_manager
# (no underscore prefix) when passed via super().__init__()
sm = getattr(self, "settings_manager", None)
try:
if sm:
device_id = sm.get_device_id("discovery", self.country)
if device_id:
logger.debug(f"Loaded device_id from settings: {device_id}")
return device_id
except Exception as e:
logger.debug(f"Could not load device_id from settings manager: {e}")
logger.debug("No settings manager available, using DEFAULT_DEVICE_ID")
return DEFAULT_DEVICE_ID
def _discover_endpoints(self, auth_headers: Dict[str, str]) -> None:
"""
Discover API endpoints from bootstrap using an authenticated session.
@@ -57,7 +57,7 @@ class PlatformOS(str, Enum):
# 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
DEFAULT_PLATFORM_OS: Final[PlatformOS] = PlatformOS.LINUX
# ============================================================================
@@ -186,6 +186,8 @@ DISCOVERY_DEVICE_INFO_TEMPLATE_LINUX: Final[str] = (
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"
@@ -228,6 +230,8 @@ DISCOVERY_CLIENT_ID_PREFIX: Final[str] = "web1_prd"
# 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.
@@ -240,7 +244,10 @@ DISCOVERY_FEATURE_FLAGS_PAYLOAD: Final[Dict] = {
}
def get_default_device_info(platform_os: Optional[PlatformOS] = None) -> Dict[str, any]:
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.
@@ -248,6 +255,9 @@ def get_default_device_info(platform_os: Optional[PlatformOS] = None) -> Dict[st
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.
@@ -255,6 +265,9 @@ def get_default_device_info(platform_os: Optional[PlatformOS] = None) -> Dict[st
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"}
@@ -264,7 +277,7 @@ def get_default_device_info(platform_os: Optional[PlatformOS] = None) -> Dict[st
browser_info = {"name": "Chrome", "version": "144.0.0.0"}
return {
"deviceId": DEFAULT_DEVICE_ID,
"deviceId": device_id,
"browser": browser_info,
"make": "desktop",
"model": "desktop",
@@ -189,6 +189,19 @@ class DiscoveryProvider(StreamingProvider):
# self-constructs its own SettingsManager via the backward-compat path,
# identical to how JoynAuthenticator works. This ensures it always
# resolves the correct /config path via the environment variable.
# Resolve stable device ID from settings manager before constructing
# the authenticator — the authenticator intentionally receives
# settings_manager=None so it cannot do this lookup itself.
_device_id = None
if self._settings_manager:
try:
_device_id = self._settings_manager.get_device_id(
"discovery", country
)
logger.debug(f"Resolved device_id from settings: {_device_id}")
except Exception as e:
logger.debug(f"Could not resolve device_id from settings: {e}")
self.authenticator = DiscoveryAuthenticator(
country=country,
settings_manager=None,
@@ -197,6 +210,7 @@ class DiscoveryProvider(StreamingProvider):
proxy_config=self.http_manager.config.proxy_config,
credentials=self.credentials,
platform_os=self.platform_os,
device_id=_device_id,
)
# Authenticate
@@ -452,7 +466,10 @@ class DiscoveryProvider(StreamingProvider):
"ssaiProvider": {"version": "2.2.0"},
},
"consumptionType": "streaming",
"deviceInfo": get_default_device_info(self.platform_os),
"deviceInfo": get_default_device_info(
self.platform_os,
device_id=self.authenticator.device_id,
),
"editId": edit_id,
"capabilities": get_default_capabilities(self.platform_os),
"gdpr": False,