Add discovery

This commit is contained in:
Nirvana
2026-02-27 21:09:54 +01:00
parent f04d33d921
commit 9e39a54a68
4 changed files with 73 additions and 8 deletions
@@ -21,6 +21,7 @@ from .constants import (
SUPPORTED_COUNTRIES,
CHANNEL_COLLECTIONS,
CHANNEL_ITEM_TYPES,
DISCOVERY_USERS_ME_URL,
StreamingMode,
CDMMode,
VideoQuality,
@@ -75,6 +76,7 @@ __all__ = [
"SUPPORTED_COUNTRIES",
"CHANNEL_COLLECTIONS",
"CHANNEL_ITEM_TYPES",
"DISCOVERY_USERS_ME_URL",
# Constants - Enums
"StreamingMode",
@@ -72,6 +72,10 @@ DISCOVERY_LOGO: Final[
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"
@@ -89,10 +93,9 @@ SUPPORTED_AUTH_TYPES: Final[List[str]] = [
# Country/Region Configuration
# ============================================================================
# Supported countries (EMEA region)
SUPPORTED_COUNTRIES: Final[List[str]] = [
"de", "at", "ch", "dk", "fi", "no", "se", "it", "nl", "es", "uk", "ie",
]
# Wildcard sentinel — Discovery+ accepts any country; the user's actual country
# is determined at runtime via the /users/me endpoint (currentLocationTerritory).
SUPPORTED_COUNTRIES: Final[str] = "*"
# Home market mapping (immutable)
HOME_MARKET_MAPPING: Final[Dict[str, str]] = {
@@ -28,6 +28,7 @@ from .constants import (
DEFAULT_TENANT,
DEFAULT_PLATFORM_OS,
DISCOVERY_LOGO,
DISCOVERY_USERS_ME_URL,
DRMSystem as DiscoveryDRMSystem,
DRM_SYSTEM_PLAYREADY,
PlatformOS,
@@ -71,7 +72,7 @@ class DiscoveryProvider(StreamingProvider):
PROVIDER_LABEL: ClassVar[str] = "Discovery+"
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = SUPPORTED_AUTH_TYPES
PROVIDER_LOGO: ClassVar[str] = DISCOVERY_LOGO
SUPPORTED_COUNTRIES: ClassVar[List[str]] = SUPPORTED_COUNTRIES
SUPPORTED_COUNTRIES: ClassVar[str] = SUPPORTED_COUNTRIES # "*" = all countries
def __init__(
self,
@@ -106,9 +107,9 @@ class DiscoveryProvider(StreamingProvider):
ValueError: If country not supported or credentials missing
"""
if not self.validate_country(country):
supported = ", ".join(self.SUPPORTED_COUNTRIES)
raise ValueError(
f"Unsupported country: {country}. Discovery+ supports: {supported}"
f"Unsupported country: {country}. "
f"Provide a valid ISO-3166-1 alpha-2 country code (e.g. 'de', 'uk')."
)
super().__init__(country=country)
@@ -300,6 +301,64 @@ class DiscoveryProvider(StreamingProvider):
return self.token_info.get("anonymous", True)
return True
def validate_country(self, country: str) -> bool:
"""
Override base validation.
Discovery+ supports all countries (SUPPORTED_COUNTRIES == "*"),
so any non-empty lowercase alpha-2 code is accepted.
The user's actual serving country is determined at runtime via
get_user_country().
"""
if self.SUPPORTED_COUNTRIES == "*":
return bool(country and country.isalpha() and len(country) == 2)
# Fallback to standard list-based check if constant is ever changed back
return country.lower() in [c.lower() for c in self.SUPPORTED_COUNTRIES]
def get_user_country(self) -> Optional[str]:
"""
Detect the user's current country by calling the /users/me endpoint.
The API returns ``currentLocationTerritory`` (e.g. ``"DE"``) inside
``data.attributes``. This method normalises the value to lowercase
so it is consistent with the rest of the codebase (e.g. ``"de"``).
Returns:
Two-letter lowercase country code (e.g. ``"de"``, ``"uk"``) or
``None`` if the request fails or the field is absent.
Example::
provider = DiscoveryProvider(country="de")
country = provider.get_user_country()
# → "de" (or whatever territory the server reports)
"""
try:
headers = self._get_auth_headers()
response = self.http_manager.get(
DISCOVERY_USERS_ME_URL,
operation="users_me",
headers=headers,
)
response.raise_for_status()
data = response.json()
territory = (
data.get("data", {})
.get("attributes", {})
.get("currentLocationTerritory")
)
if territory:
country_code = territory.lower()
logger.debug(
f"Detected user country from /users/me: {country_code}"
)
return country_code
logger.warning("/users/me response missing currentLocationTerritory")
return None
except Exception as e:
logger.error(f"Failed to fetch user country from /users/me: {e}")
return None
def get_channels(
self,
fetch_manifests: bool = False,
@@ -17,6 +17,7 @@ class RTLPlusDefaults:
# Device information
DEVICE_ID = "8c3f37cc-13a3-4141-bd0f-e4b3673fe5e4"
DEVICE_NAME = "Linux Chrome"
PLAYREADY_DEVICE_NAME = "Windows Edge"
# User Agent components
USER_AGENT = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{CHROME_VERSION} Safari/537.36"
@@ -132,7 +133,7 @@ class RTLPlusHeaders:
"User-Agent": RTLPlusDefaults.PLAYREADY_USER_AGENT,
"X-Auth-Token": access_token,
"X-Device-Id": device_id or RTLPlusDefaults.DEVICE_ID,
"X-Device-Name": RTLPlusDefaults.DEVICE_NAME,
"X-Device-Name": RTLPlusDefaults.PLAYREADY_DEVICE_NAME,
}