mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-25 16:22:33 +01:00
415 lines
17 KiB
Python
415 lines
17 KiB
Python
# streaming_providers/providers/magenta2/discovery.py
|
|
import time
|
|
from typing import Any, Dict, Optional
|
|
|
|
from ...base.models.proxy_models import ProxyConfig
|
|
from ...base.network import HTTPManager
|
|
from ...base.utils.logger import logger
|
|
from .config_models import BootstrapConfig, ManifestConfig, OpenIDConfig, ProviderConfig
|
|
from .constants import (
|
|
BOOTSTRAP_CACHE_DURATION,
|
|
DEFAULT_REQUEST_TIMEOUT,
|
|
MAGENTA2_BOOTSTRAP_URL,
|
|
MAGENTA2_MANIFEST_URL,
|
|
MAGENTA2_PLATFORMS,
|
|
OPENID_CONFIG_CACHE_DURATION,
|
|
render_user_agent,
|
|
)
|
|
|
|
|
|
class DiscoveryService:
|
|
"""
|
|
Service for dynamic discovery of Magenta2 endpoints and configuration
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
platform: str,
|
|
device_id: str,
|
|
session_id: str,
|
|
http_manager: HTTPManager,
|
|
proxy_config: Optional[ProxyConfig] = None,
|
|
user_type: str = "normal",
|
|
):
|
|
cfg = MAGENTA2_PLATFORMS[platform]
|
|
self.platform = platform
|
|
self._config_group = cfg["config_group"]
|
|
self._subscriber_type = cfg["subscriber_type"]
|
|
self._application_model = cfg["application_model"]
|
|
self._api_level = cfg["api_level"]
|
|
self._version = cfg["version"]
|
|
self._ua_plain = render_user_agent(platform, subscriber_suffix=False)
|
|
self._ua_subscriber = render_user_agent(platform, subscriber_suffix=True)
|
|
self.device_id = device_id
|
|
self.session_id = session_id
|
|
self.http_manager = http_manager
|
|
self.proxy_config = proxy_config
|
|
self._user_type = user_type
|
|
|
|
# Cache storage
|
|
self._bootstrap_config: Optional[BootstrapConfig] = None
|
|
self._manifest_config: Optional[ManifestConfig] = None
|
|
self._openid_config: Optional[OpenIDConfig] = None
|
|
self._last_bootstrap: Optional[float] = None
|
|
self._last_manifest: Optional[float] = None
|
|
self._last_openid: Optional[float] = None
|
|
|
|
def discover_provider_config(self, force_refresh: bool = False) -> ProviderConfig:
|
|
"""
|
|
Perform complete provider configuration discovery
|
|
|
|
Returns:
|
|
ProviderConfig: Complete provider configuration
|
|
"""
|
|
logger.info("Starting Magenta2 provider configuration discovery")
|
|
|
|
try:
|
|
# Step 1: Bootstrap discovery
|
|
bootstrap_config = self.discover_bootstrap(force_refresh)
|
|
if not bootstrap_config:
|
|
raise Exception("Bootstrap discovery failed - cannot proceed")
|
|
|
|
# Step 2: Manifest discovery (includes device token)
|
|
manifest_config = self.discover_manifest(force_refresh)
|
|
|
|
# IMPORTANT: Update bootstrap with manifest data
|
|
if manifest_config:
|
|
bootstrap_config.update_from_manifest(manifest_config)
|
|
|
|
# Log device token status
|
|
device_token = manifest_config.get_device_token()
|
|
if device_token:
|
|
logger.info("✓ Device token obtained from manifest")
|
|
else:
|
|
logger.warning("⚠️ No device token found in manifest")
|
|
|
|
# Step 3: OpenID discovery (if bootstrap provided OpenID config URL)
|
|
openid_config = None
|
|
if bootstrap_config.openid_config_url:
|
|
try:
|
|
openid_config = self.discover_openid_config(force_refresh)
|
|
except Exception as e:
|
|
logger.warning(f"OpenID discovery failed: {e}")
|
|
|
|
provider_config = ProviderConfig(
|
|
bootstrap=bootstrap_config,
|
|
manifest=manifest_config,
|
|
openid=openid_config,
|
|
)
|
|
|
|
# Log MPX account info for persona token composition
|
|
if manifest_config and manifest_config.mpx:
|
|
account_uri = manifest_config.mpx.get_account_uri()
|
|
logger.info(f"✓ MPX account URI for persona token: {account_uri}")
|
|
|
|
return provider_config
|
|
|
|
except Exception as e:
|
|
logger.error(f"Configuration discovery failed: {e}")
|
|
self._create_fallback_configuration()
|
|
raise
|
|
|
|
def discover_bootstrap(self, force_refresh: bool = False) -> Optional[BootstrapConfig]:
|
|
"""
|
|
Discover bootstrap configuration
|
|
|
|
Returns:
|
|
BootstrapConfig: Bootstrap configuration, None if failed
|
|
"""
|
|
# Check cache
|
|
if not force_refresh and self._bootstrap_config and self._last_bootstrap:
|
|
cache_age = time.time() - self._last_bootstrap
|
|
if cache_age < BOOTSTRAP_CACHE_DURATION:
|
|
logger.debug("Using cached bootstrap configuration")
|
|
return self._bootstrap_config
|
|
|
|
try:
|
|
logger.info("Discovering bootstrap configuration")
|
|
|
|
url = MAGENTA2_BOOTSTRAP_URL.format(config_group=self._config_group)
|
|
|
|
# Superset of the modern web-client shape (deviceId, portal,
|
|
# subscriberType, $redirect, sid — from MacBook web capture) and
|
|
# the legacy/ATV shape (applicationModel, version — confirmed
|
|
# against real AndroidTV request logs, v3.180.7748). deviceModel
|
|
# is intentionally NOT sent on bootstrap: it's absent from every
|
|
# modern-shape capture we have (web and ATV alike) and only
|
|
# appears on the legacy ATV bootstrap shape.
|
|
params = {
|
|
"deviceId": self.device_id,
|
|
"portal": "release",
|
|
"subscriberType": self._subscriber_type,
|
|
"$redirect": "false",
|
|
"sid": self.session_id,
|
|
"applicationModel": self._application_model,
|
|
"version": self._version,
|
|
}
|
|
|
|
headers = self._get_dcm_headers(subscriber_suffix=False)
|
|
|
|
response = self.http_manager.get(
|
|
url,
|
|
operation="bootstrap",
|
|
headers=headers,
|
|
params=params,
|
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
bootstrap_data = response.json()
|
|
self._bootstrap_config = BootstrapConfig.from_api_response(
|
|
bootstrap_data, self.platform
|
|
)
|
|
self._last_bootstrap = time.time()
|
|
|
|
logger.info(
|
|
f"Bootstrap discovery successful: "
|
|
f"clientModel={self._bootstrap_config.client_model}, "
|
|
f"deviceModel={self._bootstrap_config.device_model}"
|
|
)
|
|
|
|
# Log critical endpoints
|
|
if self._bootstrap_config.taa_url:
|
|
logger.debug(f"TAA URL: {self._bootstrap_config.taa_url}")
|
|
if self._bootstrap_config.device_tokens_url:
|
|
logger.debug(f"Device tokens URL: {self._bootstrap_config.device_tokens_url}")
|
|
|
|
return self._bootstrap_config
|
|
|
|
except Exception as e:
|
|
logger.error(f"Bootstrap discovery failed: {e}")
|
|
# Don't cache failed attempts
|
|
self._bootstrap_config = None
|
|
self._last_bootstrap = None
|
|
return None
|
|
|
|
def discover_manifest(self, force_refresh: bool = False) -> Optional[ManifestConfig]:
|
|
"""
|
|
ENHANCED: Discover manifest configuration including device token
|
|
Uses correct manifest endpoint parameters
|
|
|
|
Returns:
|
|
ManifestConfig: Manifest configuration, None if failed
|
|
"""
|
|
# Check cache
|
|
if not force_refresh and self._manifest_config and self._last_manifest:
|
|
cache_age = time.time() - self._last_manifest
|
|
if cache_age < BOOTSTRAP_CACHE_DURATION:
|
|
logger.debug("Using cached manifest configuration")
|
|
return self._manifest_config
|
|
|
|
try:
|
|
logger.info("Discovering manifest configuration")
|
|
|
|
# Manifest discovery only runs after a successful bootstrap — it
|
|
# needs deviceModel from the bootstrap response.
|
|
if not self._bootstrap_config:
|
|
logger.error("Cannot discover manifest: no bootstrap config available")
|
|
return None
|
|
|
|
# Priority 1: Use dcm.manifestBaseUrl from bootstrap
|
|
if self._bootstrap_config.manifest_base_url:
|
|
manifest_url = self._bootstrap_config.manifest_base_url.replace(
|
|
"{configGroupId}", self._config_group
|
|
)
|
|
logger.debug(f"Using bootstrap manifestBaseUrl: {manifest_url}")
|
|
|
|
# Priority 2: Fallback to hardcoded manifest URL
|
|
else:
|
|
manifest_url = MAGENTA2_MANIFEST_URL.format(config_group=self._config_group)
|
|
logger.debug(f"Using fallback manifest URL: {manifest_url}")
|
|
|
|
# Superset of the modern web-client shape (deviceId, deviceModel,
|
|
# portal, subscriberType, $redirect, sid — from MacBook web
|
|
# capture) and the legacy/ATV shape (applicationModel, version,
|
|
# apiLevel, userType — confirmed against real AndroidTV request
|
|
# logs, v3.180.7748).
|
|
params = {
|
|
"deviceId": self.device_id,
|
|
"portal": "release",
|
|
"subscriberType": self._subscriber_type,
|
|
"$redirect": "false",
|
|
"sid": self.session_id,
|
|
"deviceModel": self._bootstrap_config.device_model,
|
|
"applicationModel": self._application_model,
|
|
"version": self._version,
|
|
"apiLevel": self._api_level,
|
|
"userType": self._user_type,
|
|
}
|
|
|
|
logger.debug(f"Manifest request params: {params}")
|
|
|
|
headers = self._get_dcm_headers(subscriber_suffix=False)
|
|
|
|
response = self.http_manager.get(
|
|
manifest_url,
|
|
operation="manifest_discovery",
|
|
headers=headers,
|
|
params=params,
|
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
manifest_data = response.json()
|
|
self._manifest_config = ManifestConfig.from_api_response(manifest_data)
|
|
self._last_manifest = time.time()
|
|
|
|
# ENHANCED: Log device token and critical auth info
|
|
device_token = self._manifest_config.get_device_token()
|
|
authorize_tokens_url = self._manifest_config.get_authorize_tokens_url()
|
|
|
|
if device_token:
|
|
logger.info("✓ Device token found in manifest")
|
|
logger.debug(f"Device token preview: {device_token[:20]}...{device_token[-10:]}")
|
|
else:
|
|
logger.warning("⚠️ Device token NOT found in manifest response")
|
|
|
|
if authorize_tokens_url:
|
|
logger.debug(f"Authorize tokens URL: {authorize_tokens_url}")
|
|
|
|
# Log MPX account info (critical for persona token)
|
|
mpx_account_pid = self._manifest_config.mpx.account_pid
|
|
account_uri = self._manifest_config.mpx.get_account_uri()
|
|
logger.info(f"MPX account PID: {mpx_account_pid}")
|
|
logger.info(f"MPX account URI: {account_uri}")
|
|
|
|
logger.info(
|
|
f"Manifest discovery successful: "
|
|
f"MPX account={mpx_account_pid}, "
|
|
f"DRM endpoints={len([k for k in self._manifest_config.drm.__dict__.keys() if self._manifest_config.drm.__dict__[k]])}, "
|
|
f"TV hubs={len(self._manifest_config.tv_hubs.base_urls)}"
|
|
)
|
|
|
|
return self._manifest_config
|
|
|
|
except Exception as e:
|
|
logger.error(f"Manifest discovery failed: {e}")
|
|
# Don't cache failed attempts
|
|
self._manifest_config = None
|
|
self._last_manifest = None
|
|
return None
|
|
|
|
def discover_openid_config(self, force_refresh: bool = False) -> Optional[OpenIDConfig]:
|
|
"""
|
|
Discover OpenID Connect configuration
|
|
|
|
Returns:
|
|
OpenIDConfig: OpenID configuration, None if failed
|
|
"""
|
|
# Check cache
|
|
if not force_refresh and self._openid_config and self._last_openid:
|
|
cache_age = time.time() - self._last_openid
|
|
if cache_age < OPENID_CONFIG_CACHE_DURATION:
|
|
logger.debug("Using cached OpenID configuration")
|
|
return self._openid_config
|
|
|
|
try:
|
|
if not self._bootstrap_config or not self._bootstrap_config.openid_config_url:
|
|
logger.warning("No OpenID config URL available from bootstrap")
|
|
return None
|
|
|
|
logger.info("Discovering OpenID configuration")
|
|
|
|
openid_url = self._bootstrap_config.openid_config_url
|
|
|
|
response = self.http_manager.get(
|
|
openid_url,
|
|
operation="openid_discovery",
|
|
headers={"Accept": "application/json"},
|
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
openid_data = response.json()
|
|
self._openid_config = OpenIDConfig.from_api_response(openid_data)
|
|
self._last_openid = time.time()
|
|
|
|
logger.info(
|
|
f"OpenID discovery successful: token_endpoint={self._openid_config.token_endpoint}"
|
|
)
|
|
return self._openid_config
|
|
|
|
except Exception as e:
|
|
logger.error(f"OpenID discovery failed: {e}")
|
|
# Don't cache failed attempts
|
|
self._openid_config = None
|
|
self._last_openid = None
|
|
return None
|
|
|
|
def _get_dcm_headers(self, subscriber_suffix: bool = False) -> Dict[str, str]:
|
|
"""Get headers for DCM requests (bootstrap/manifest)."""
|
|
ua = self._ua_subscriber if subscriber_suffix else self._ua_plain
|
|
return {
|
|
"User-Agent": ua,
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
"Accept-Encoding": "gzip",
|
|
"x-dt-session-id": self.session_id,
|
|
"x-dt-call-id": self._generate_call_id(),
|
|
}
|
|
|
|
@staticmethod
|
|
def _generate_call_id() -> str:
|
|
"""Generate call ID for requests"""
|
|
import uuid
|
|
|
|
return str(uuid.uuid4())
|
|
|
|
def get_discovery_status(self) -> Dict[str, Any]:
|
|
"""Get discovery service status"""
|
|
status = {
|
|
"bootstrap_available": self._bootstrap_config is not None,
|
|
"manifest_available": self._manifest_config is not None,
|
|
"openid_available": self._openid_config is not None,
|
|
}
|
|
|
|
if self._bootstrap_config:
|
|
status["bootstrap"] = {
|
|
"client_model": self._bootstrap_config.client_model,
|
|
"device_model": self._bootstrap_config.device_model,
|
|
"has_device_tokens_url": bool(self._bootstrap_config.device_tokens_url),
|
|
"has_openid_config_url": bool(self._bootstrap_config.openid_config_url),
|
|
"has_taa_url": bool(self._bootstrap_config.taa_url),
|
|
}
|
|
|
|
if self._manifest_config:
|
|
device_token = self._manifest_config.get_device_token()
|
|
status["manifest"] = {
|
|
"mpx_account_pid": self._manifest_config.mpx.account_pid,
|
|
"mpx_account_uri": self._manifest_config.mpx.get_account_uri(),
|
|
"feed_count": len(self._manifest_config.mpx.feeds),
|
|
"has_device_token": bool(device_token),
|
|
"device_token_preview": (device_token[:20] + "..." if device_token else None),
|
|
"drm_endpoints": {
|
|
"widevine": bool(self._manifest_config.drm.widevine_license_url),
|
|
"vod_widevine": bool(self._manifest_config.drm.vod_widevine_license_url),
|
|
"fairplay": bool(self._manifest_config.drm.fairplay_license_url),
|
|
},
|
|
"tvhub_count": len(self._manifest_config.tv_hubs.base_urls),
|
|
}
|
|
|
|
if self._openid_config:
|
|
status["openid"] = {
|
|
"has_token_endpoint": bool(self._openid_config.token_endpoint),
|
|
"has_authorization_endpoint": bool(self._openid_config.authorization_endpoint),
|
|
}
|
|
|
|
# Cache status
|
|
now = time.time()
|
|
status["cache"] = {
|
|
"bootstrap_age": (now - self._last_bootstrap if self._last_bootstrap else None),
|
|
"manifest_age": now - self._last_manifest if self._last_manifest else None,
|
|
"openid_age": now - self._last_openid if self._last_openid else None,
|
|
}
|
|
|
|
return status
|
|
|
|
def clear_cache(self) -> None:
|
|
"""Clear all discovery cache"""
|
|
self._bootstrap_config = None
|
|
self._manifest_config = None
|
|
self._openid_config = None
|
|
self._last_bootstrap = None
|
|
self._last_manifest = None
|
|
self._last_openid = None
|
|
logger.info("Discovery cache cleared") |