mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-27 03:22:48 +02:00
RTL: add device login
This commit is contained in:
@@ -44,7 +44,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
http_manager=http_manager,
|
||||
)
|
||||
|
||||
# ✅ AFTER super().__init__ so the base class can't overwrite it
|
||||
# AFTER super().__init__ so the base class can't overwrite it
|
||||
self._config = RTLPlusConfig(config_dict)
|
||||
self._client_id = None
|
||||
self._bedrock_token: Optional[str] = None
|
||||
@@ -66,7 +66,15 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
|
||||
@property
|
||||
def oauth_client_id(self) -> str:
|
||||
return RTLPlusDefaults.BEDROCK_CLIENT_ID # "bedrock-m6group_web"
|
||||
# Dynamic: refresh_token grants must be replayed against the same
|
||||
# client_id the token was originally issued under. Web login uses
|
||||
# BEDROCK_CLIENT_ID; the device/QR flow uses DEVICE_CLIENT_ID.
|
||||
# _build_refresh_payload() (base class, base_oauth2_auth.py) reads
|
||||
# this property, so keeping it dynamic is what makes refresh work
|
||||
# for both flows without overriding _build_refresh_payload().
|
||||
current = getattr(self, "_current_token", None)
|
||||
login_client = getattr(current, "login_client", None)
|
||||
return login_client or RTLPlusDefaults.BEDROCK_CLIENT_ID
|
||||
|
||||
@property
|
||||
def oauth_scope(self) -> str:
|
||||
@@ -106,6 +114,15 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
return RTLPlusClientCredentials()
|
||||
|
||||
def _create_token_from_response(self, response_data: Dict[str, Any]) -> RTLPlusAuthToken:
|
||||
# Carry forward login_client so refresh keeps using the right
|
||||
# client_id. Normal OAuth token responses never include this key
|
||||
# (it's our own bookkeeping); if absent, fall back to whatever
|
||||
# client issued the token being replaced.
|
||||
login_client = response_data.get("login_client")
|
||||
if not login_client:
|
||||
current = getattr(self, "_current_token", None)
|
||||
login_client = getattr(current, "login_client", None)
|
||||
|
||||
return RTLPlusAuthToken(
|
||||
access_token=response_data["access_token"],
|
||||
token_type=response_data.get("token_type", "Bearer"),
|
||||
@@ -115,6 +132,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
refresh_expires_in=response_data.get("refresh_expires_in", 0),
|
||||
not_before_policy=response_data.get("not-before-policy"),
|
||||
scope=response_data.get("scope", ""),
|
||||
login_client=login_client,
|
||||
)
|
||||
|
||||
def get_current_token_level(self) -> TokenAuthLevel:
|
||||
@@ -171,9 +189,9 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
extra_params={
|
||||
"prompt": "login",
|
||||
"nonce": str(uuid.uuid4()),
|
||||
"claim": "sub", # ← add this
|
||||
"state": '{"redirectUrl":"#"}', # ← add this
|
||||
"auth_flow_type": "login", # ← add this
|
||||
"claim": "sub",
|
||||
"state": '{"redirectUrl":"#"}',
|
||||
"auth_flow_type": "login",
|
||||
},
|
||||
additional_form_data={"credentialId": "", "rememberMe": "on"},
|
||||
)
|
||||
@@ -284,17 +302,10 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
|
||||
Based on analysis, this appears to be a SHA-1 hash of device_id + timestamp.
|
||||
"""
|
||||
# Convert timestamp to string
|
||||
ts_str = str(timestamp)
|
||||
|
||||
# Try SHA-1 of device_id + timestamp (most likely)
|
||||
data = f"{device_id}{ts_str}"
|
||||
token = hashlib.sha1(data.encode()).hexdigest()
|
||||
|
||||
# Alternative: Try with colon separator
|
||||
# data = f"{device_id}:{ts_str}"
|
||||
# token = hashlib.sha1(data.encode()).hexdigest()
|
||||
|
||||
logger.debug(f"Generated auth token for device {device_id} at {timestamp}: {token}")
|
||||
return token
|
||||
|
||||
@@ -310,29 +321,24 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
if not oauth_token:
|
||||
raise ValueError("No OAuth token available for Bedrock token request")
|
||||
|
||||
# Get user ID (Gigya UID)
|
||||
user_id = self.get_user_id_from_token()
|
||||
if not user_id:
|
||||
logger.warning("No user ID available for Bedrock token request")
|
||||
# Continue without user_id - might still work for anonymous?
|
||||
|
||||
# Get timestamp and generate auth token
|
||||
timestamp = self._get_server_timestamp()
|
||||
auth_token = self._generate_auth_token(self.config.device_id, timestamp)
|
||||
|
||||
logger.debug(f"Timestamp: {timestamp}, Auth token: {auth_token}")
|
||||
|
||||
# Get profile ID if available
|
||||
profile_id = self.get_selected_profile_id()
|
||||
logger.debug(f"Using profile_id: {profile_id}")
|
||||
|
||||
# Get headers with profile_id and user_id
|
||||
headers = self.config.get_bedrock_token_headers(
|
||||
oauth_token=oauth_token,
|
||||
auth_token=auth_token,
|
||||
timestamp=timestamp,
|
||||
profile_id=profile_id,
|
||||
user_id=user_id, # Add user_id parameter
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
@@ -363,7 +369,6 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
f"Bedrock token obtained with profile: {decoded.get('profileid', 'none')}, expires: {self._bedrock_token_expiry}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not decode Bedrock token expiry: {e}")
|
||||
# Set default expiry (1 hour)
|
||||
self._bedrock_token_expiry = time.time() + 3600
|
||||
|
||||
logger.debug("RTL+ Bedrock token obtained successfully")
|
||||
@@ -375,10 +380,6 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
|
||||
The upfront token is used as x-dt-auth-token when requesting
|
||||
licenses from DRMToday.
|
||||
|
||||
Args:
|
||||
content_id: DRM content ID (e.g., "dashcenc_rtlde_vox")
|
||||
uid: User ID from OAuth token
|
||||
"""
|
||||
oauth_token = self.get_bearer_token()
|
||||
bedrock_token = self.get_bedrock_token()
|
||||
@@ -406,9 +407,6 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
def get_user_id_from_token(self) -> Optional[str]:
|
||||
"""
|
||||
Extract user ID (sub claim) from the current OAuth token.
|
||||
|
||||
Returns:
|
||||
User ID string or None if not available
|
||||
"""
|
||||
if self._cached_user_id:
|
||||
return self._cached_user_id
|
||||
@@ -430,11 +428,9 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
payload_json = base64.b64decode(payload_segment)
|
||||
payload = json.loads(payload_json)
|
||||
|
||||
# The user ID is in the 'sub' claim
|
||||
# Format: f:83a2e227-f27d-4d33-a811-33ad588170c4:1052940424
|
||||
sub = payload.get("sub", "")
|
||||
|
||||
# Extract numeric ID from the end if present
|
||||
if ":" in sub:
|
||||
self._cached_user_id = sub.split(":")[-1]
|
||||
else:
|
||||
@@ -465,7 +461,7 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
logger.info("RTL+ user credentials saved successfully")
|
||||
self.invalidate_token()
|
||||
self.invalidate_bedrock_token()
|
||||
self._cached_user_id = None # Clear cached user ID
|
||||
self._cached_user_id = None
|
||||
else:
|
||||
logger.error("Failed to save RTL+ user credentials")
|
||||
|
||||
@@ -540,7 +536,6 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
profiles = self.get_user_profiles(user_id)
|
||||
logger.debug(f"Fetched profiles: {profiles}")
|
||||
|
||||
# Select first adult profile (not kid profile)
|
||||
adult_profiles = [p for p in profiles if p.get("profile_type") == "adult"]
|
||||
if not adult_profiles:
|
||||
logger.error("No adult profiles found")
|
||||
@@ -553,15 +548,12 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
logger.error(f"Failed to fetch/select profile: {e}")
|
||||
return False
|
||||
|
||||
# Store the selected profile ID
|
||||
self._selected_profile_id = profile_id
|
||||
|
||||
# Save to credentials for persistence
|
||||
if hasattr(self.credentials, "profile_id"):
|
||||
self.credentials.profile_id = profile_id
|
||||
self.save_credentials(self.credentials)
|
||||
|
||||
# Invalidate Bedrock token so it gets re-issued with the profile ID
|
||||
self.invalidate_bedrock_token()
|
||||
|
||||
logger.info(f"Profile selected successfully: {profile_id}")
|
||||
@@ -572,7 +564,6 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
if hasattr(self, "_selected_profile_id") and self._selected_profile_id:
|
||||
return self._selected_profile_id
|
||||
|
||||
# Try to load from stored credentials
|
||||
if hasattr(self.credentials, "profile_id") and self.credentials.profile_id:
|
||||
return self.credentials.profile_id
|
||||
|
||||
@@ -586,4 +577,42 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator):
|
||||
if self.has_user_credentials():
|
||||
return self.select_profile()
|
||||
|
||||
return False
|
||||
return False
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Remote Login (Device Code / QR) — overrides base's PKCE remote login,
|
||||
# which uses a different protocol RTL+ does not support for this flow.
|
||||
# See remote_login_handler.py in this package for the implementation.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _perform_remote_login_flow(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Called automatically by the base class's authenticate_with_fallback()
|
||||
when _perform_oauth_authorization_code_flow() raises WafBlockedException.
|
||||
|
||||
RTL+'s remote login is a genuine OAuth2 Device Authorization Grant
|
||||
(RFC 8628) against a dedicated TV/device client, not the PKCE
|
||||
authorization_code + callback flow the base class implements by
|
||||
default (BaseOAuth2Authenticator._perform_remote_login_flow /
|
||||
RemoteLoginManager) — that flow's redirect_uri isn't reachable
|
||||
from a phone completing the login, so it doesn't apply here.
|
||||
"""
|
||||
from .remote_login_handler import RTLRemoteLoginHandler
|
||||
|
||||
handler = RTLRemoteLoginHandler(
|
||||
http_manager=self.http_manager,
|
||||
device_client_id=RTLPlusDefaults.DEVICE_CLIENT_ID,
|
||||
device_auth_url=RTLPlusDefaults.DEVICE_AUTH_ENDPOINT,
|
||||
token_endpoint=RTLPlusDefaults.AUTH_ENDPOINT,
|
||||
provider_name="RTL+",
|
||||
)
|
||||
|
||||
token_data = handler.perform_complete_flow()
|
||||
|
||||
if not token_data:
|
||||
raise Exception("RTL+ remote login failed")
|
||||
|
||||
# Tag which client issued this token so oauth_client_id /
|
||||
# _create_token_from_response can route refreshes correctly.
|
||||
token_data.setdefault("login_client", RTLPlusDefaults.DEVICE_CLIENT_ID)
|
||||
return token_data
|
||||
@@ -38,11 +38,21 @@ class RTLPlusDefaults:
|
||||
CLIENT_VERSION_FALLBACK = "2025.6.26.0"
|
||||
CLIENT_ID = f"rtlplus-{PLATFORM_DEFAULT}"
|
||||
BEDROCK_CLIENT_ID = "bedrock-m6group_web"
|
||||
# Dedicated client for the OAuth2 Device Authorization Grant (RFC 8628).
|
||||
# Must NOT be BEDROCK_CLIENT_ID -- /auth/device only accepts clients
|
||||
# registered for the device-code flow. Confirmed against the working
|
||||
# legacy addon (auth.py: DEVICE_CLIENT_ID = 'bedrock-androidtv').
|
||||
DEVICE_CLIENT_ID = "bedrock-androidtv"
|
||||
|
||||
# API endpoints
|
||||
AUTH_REALM_BASE = "https://auth.rtl.de/auth/realms/rtlplus"
|
||||
AUTH_BASE_URL = f"{AUTH_REALM_BASE}/protocol/openid-connect"
|
||||
AUTH_ENDPOINT = f"{AUTH_BASE_URL}/token"
|
||||
# OAuth2 Device Authorization Grant endpoint (RFC 8628). Confirmed
|
||||
# correct against the working legacy addon -- it lives under
|
||||
# AUTH_BASE_URL (.../protocol/openid-connect/auth/device), NOT under
|
||||
# AUTH_REALM_BASE directly.
|
||||
DEVICE_AUTH_ENDPOINT = f"{AUTH_BASE_URL}/auth/device"
|
||||
GRAPHQL_ENDPOINT = "https://cdn.gateway.now-plus-prod.aws-cbc.cloud/graphql"
|
||||
BASE_WEBSITE = "https://plus.rtl.de/"
|
||||
CONFIG_ENDPOINT = "https://plus.rtl.de/assets/config/config.json"
|
||||
|
||||
@@ -64,6 +64,7 @@ class RTLPlusAuthToken(BaseAuthToken):
|
||||
refresh_expires_in: int = 0,
|
||||
not_before_policy: Optional[int] = None,
|
||||
scope: str = "",
|
||||
login_client: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
access_token=access_token,
|
||||
@@ -76,6 +77,13 @@ class RTLPlusAuthToken(BaseAuthToken):
|
||||
self.refresh_expires_in = refresh_expires_in
|
||||
self.not_before_policy = not_before_policy
|
||||
self.scope = scope
|
||||
# Which OAuth client_id this token (and its refresh_token) was
|
||||
# issued under. RTL+ registers separate clients per flow
|
||||
# (BEDROCK_CLIENT_ID for web login, DEVICE_CLIENT_ID for the
|
||||
# device-code/QR flow) and refresh_token grants must be replayed
|
||||
# against the SAME client_id or the auth server rejects them.
|
||||
# Mirrors the legacy addon's `login_client` tracking.
|
||||
self.login_client = login_client
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert token to dictionary"""
|
||||
@@ -88,6 +96,7 @@ class RTLPlusAuthToken(BaseAuthToken):
|
||||
"refresh_expires_in": self.refresh_expires_in,
|
||||
"not_before_policy": self.not_before_policy,
|
||||
"scope": self.scope,
|
||||
"login_client": self.login_client,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -102,6 +111,7 @@ class RTLPlusAuthToken(BaseAuthToken):
|
||||
refresh_expires_in=data.get("refresh_expires_in", 0),
|
||||
not_before_policy=data.get("not-before-policy"),
|
||||
scope=data.get("scope", ""),
|
||||
login_client=data.get("login_client"),
|
||||
)
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# streaming_providers/providers/rtlplus/remote_login_handler.py
|
||||
"""
|
||||
Remote Login Handler for RTL+ — OAuth2 Device Authorization Grant (RFC 8628)
|
||||
|
||||
Same architecture as providers/magenta2/remote_login_handler.py: a
|
||||
provider-specific handler that owns the protocol details, and delegates
|
||||
QR display / countdown / polling-thread lifecycle to the generic
|
||||
NotificationFactory adapters. No third-party QR service is used — the
|
||||
Kodi adapter renders the QR locally via qr_generator.generate_qr_code_png,
|
||||
and the console adapter just prints the URL/code.
|
||||
|
||||
Endpoint and client_id are confirmed against the working legacy Kodi
|
||||
addon's _device_login() implementation:
|
||||
- device auth: POST {AUTH_BASE_URL}/auth/device (NOT AUTH_REALM_BASE)
|
||||
- client_id: 'bedrock-androidtv' (NOT the web client)
|
||||
- token poll: POST {AUTH_ENDPOINT} with
|
||||
grant_type=urn:ietf:params:oauth:grant-type:device_code
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ...base.network import HTTPManager
|
||||
from ...base.ui import NotificationFactory, NotificationInterface, NotificationResult
|
||||
from ...base.utils.logger import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class RTLDeviceLoginSession:
|
||||
"""RTL+ device-code login session data"""
|
||||
|
||||
device_code: str
|
||||
user_code: str
|
||||
verification_uri: str
|
||||
verification_uri_complete: str
|
||||
expires_in: int
|
||||
interval: int
|
||||
started_at: float
|
||||
|
||||
|
||||
class RTLRemoteLoginHandler:
|
||||
"""
|
||||
Handles the OAuth2 Device Authorization Grant for RTL+.
|
||||
|
||||
Responsibilities:
|
||||
- Start the device auth session (/auth/device)
|
||||
- Poll the token endpoint until the user completes login on another device
|
||||
- Coordinate with the generic notification system for QR/code display
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
http_manager: HTTPManager,
|
||||
device_client_id: str,
|
||||
device_auth_url: str,
|
||||
token_endpoint: str,
|
||||
notifier: Optional[NotificationInterface] = None,
|
||||
provider_name: str = "RTL+",
|
||||
request_timeout: int = 15,
|
||||
):
|
||||
self.http_manager = http_manager
|
||||
self.device_client_id = device_client_id
|
||||
self.device_auth_url = device_auth_url
|
||||
self.token_endpoint = token_endpoint
|
||||
self.provider_name = provider_name
|
||||
self.request_timeout = request_timeout
|
||||
|
||||
if notifier:
|
||||
self._notifier = notifier
|
||||
else:
|
||||
self._notifier = NotificationFactory.create(
|
||||
http_manager=http_manager,
|
||||
provider_name=provider_name,
|
||||
success_message=f"{provider_name} login successful",
|
||||
failure_template=f"{provider_name} login failed: {{reason}}",
|
||||
)
|
||||
|
||||
self._current_session: Optional[RTLDeviceLoginSession] = None
|
||||
|
||||
logger.debug(f"RTLRemoteLoginHandler initialized with {self._notifier.__class__.__name__}")
|
||||
|
||||
def set_notifier(self, notifier: NotificationInterface) -> None:
|
||||
"""Set custom notification interface"""
|
||||
self._notifier = notifier
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Device Authorization Grant
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def start_remote_login(self, scope: str = "openid") -> RTLDeviceLoginSession:
|
||||
"""
|
||||
Start the device authorization request.
|
||||
|
||||
Raises:
|
||||
Exception: If the device auth request fails or the response is
|
||||
missing device_code/user_code.
|
||||
"""
|
||||
logger.info("Starting RTL+ remote login (Device Authorization Grant)")
|
||||
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
payload = {"client_id": self.device_client_id, "scope": scope}
|
||||
|
||||
try:
|
||||
response = self.http_manager.post(
|
||||
self.device_auth_url,
|
||||
operation="device_auth",
|
||||
headers=headers,
|
||||
data=payload,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"RTL+ device auth request failed: {e}")
|
||||
raise Exception(f"RTL+ remote login start failed: {e}")
|
||||
|
||||
device_code = data.get("device_code", "")
|
||||
user_code = data.get("user_code", "")
|
||||
verification_uri = data.get("verification_uri", "")
|
||||
verification_uri_complete = data.get(
|
||||
"verification_uri_complete",
|
||||
f"{verification_uri}?user_code={user_code}" if verification_uri else "",
|
||||
)
|
||||
expires_in = int(data.get("expires_in", 600))
|
||||
interval = max(int(data.get("interval", 5)), 5)
|
||||
|
||||
if not device_code or not user_code:
|
||||
raise Exception("RTL+ device auth response missing device_code/user_code")
|
||||
|
||||
session = RTLDeviceLoginSession(
|
||||
device_code=device_code,
|
||||
user_code=user_code,
|
||||
verification_uri=verification_uri,
|
||||
verification_uri_complete=verification_uri_complete,
|
||||
expires_in=expires_in,
|
||||
interval=interval,
|
||||
started_at=time.time(),
|
||||
)
|
||||
self._current_session = session
|
||||
|
||||
logger.info(
|
||||
f"RTL+ device login started: user_code={user_code}, "
|
||||
f"expires_in={expires_in}s, interval={interval}s"
|
||||
)
|
||||
return session
|
||||
|
||||
def poll_for_token(self, session: RTLDeviceLoginSession) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Poll the token endpoint until the device code is confirmed, expires,
|
||||
or is denied. Intended for use as a poll_callback: returns token
|
||||
data (dict) on success, None on timeout/denial/cancellation.
|
||||
"""
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
payload = {
|
||||
"client_id": self.device_client_id,
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||
"device_code": session.device_code,
|
||||
}
|
||||
|
||||
start_time = session.started_at
|
||||
deadline = start_time + session.expires_in
|
||||
interval = session.interval
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
if now >= deadline:
|
||||
logger.warning("RTL+ device login: session expired")
|
||||
return None
|
||||
|
||||
if self._notifier.is_cancelled():
|
||||
logger.info("RTL+ device login: user cancelled")
|
||||
return None
|
||||
|
||||
try:
|
||||
response = self.http_manager.post(
|
||||
self.token_endpoint,
|
||||
operation="device_poll",
|
||||
headers=headers,
|
||||
data=payload,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"RTL+ device poll network error (will retry): {e}")
|
||||
time.sleep(min(interval, max(0.5, deadline - now)))
|
||||
continue
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info("RTL+ device login: authentication confirmed")
|
||||
return response.json()
|
||||
|
||||
if response.status_code == 400:
|
||||
try:
|
||||
error_data = response.json()
|
||||
except Exception:
|
||||
error_data = {}
|
||||
error = error_data.get("error", "")
|
||||
|
||||
if error == "authorization_pending":
|
||||
pass
|
||||
elif error == "slow_down":
|
||||
interval = min(interval + 5, 30)
|
||||
logger.debug(f"RTL+ device login: slow_down, new interval={interval}s")
|
||||
elif error == "expired_token":
|
||||
logger.info("RTL+ device login: code expired")
|
||||
return None
|
||||
elif error == "access_denied":
|
||||
logger.info("RTL+ device login: user denied")
|
||||
return None
|
||||
else:
|
||||
logger.warning(f"RTL+ device login: unexpected error '{error}'")
|
||||
return None
|
||||
else:
|
||||
logger.warning(
|
||||
f"RTL+ device login: unexpected status {response.status_code}: {response.text[:200]}"
|
||||
)
|
||||
return None
|
||||
|
||||
time.sleep(min(interval, max(0.5, deadline - time.time())))
|
||||
|
||||
def perform_complete_flow(self, scope: str = "openid") -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Perform the complete device-code flow with QR display + polling.
|
||||
|
||||
Mirrors providers/magenta2/remote_login_handler.py's
|
||||
perform_complete_flow(): uses the notifier's integrated polling
|
||||
support when available (Kodi — auto-closes the dialog on success),
|
||||
otherwise falls back to displaying the code/URL once and polling
|
||||
manually (console).
|
||||
"""
|
||||
try:
|
||||
session = self.start_remote_login(scope)
|
||||
|
||||
if hasattr(self._notifier, "show_remote_login_with_polling"):
|
||||
logger.info("Using integrated polling (threaded)")
|
||||
|
||||
def poll_callback():
|
||||
return self.poll_for_token(session)
|
||||
|
||||
result = self._notifier.show_remote_login_with_polling(
|
||||
login_code=session.user_code,
|
||||
qr_target_url=session.verification_uri_complete,
|
||||
expires_in=session.expires_in,
|
||||
interval=session.interval,
|
||||
poll_callback=poll_callback,
|
||||
)
|
||||
|
||||
if result == NotificationResult.CONTINUE:
|
||||
if hasattr(self._notifier, "get_token_data"):
|
||||
token_data = self._notifier.get_token_data()
|
||||
if token_data:
|
||||
logger.info("✓ RTL+ remote login completed")
|
||||
self._notifier.close(success=True)
|
||||
return token_data
|
||||
logger.warning("No token data available")
|
||||
return None
|
||||
logger.error("Notifier doesn't support get_token_data()")
|
||||
return None
|
||||
|
||||
logger.warning(f"RTL+ remote login result: {result}")
|
||||
return None
|
||||
|
||||
# Console adapter or other — manual polling
|
||||
logger.info("Using manual polling")
|
||||
|
||||
result = self._notifier.show_remote_login(
|
||||
login_code=session.user_code,
|
||||
qr_target_url=session.verification_uri_complete,
|
||||
expires_in=session.expires_in,
|
||||
interval=session.interval,
|
||||
)
|
||||
|
||||
if result != NotificationResult.CONTINUE:
|
||||
logger.warning(f"Failed to show notification: {result}")
|
||||
return None
|
||||
|
||||
token_data = self.poll_for_token(session)
|
||||
|
||||
if token_data:
|
||||
logger.info("✓ RTL+ remote login completed")
|
||||
self._notifier.close(success=True)
|
||||
else:
|
||||
logger.warning("RTL+ remote login timed out or cancelled")
|
||||
self._notifier.close(success=False, message="Timeout or cancelled")
|
||||
|
||||
return token_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"RTL+ remote login flow failed: {e}")
|
||||
self._notifier.close(success=False, message=str(e))
|
||||
return None
|
||||
finally:
|
||||
self._current_session = None
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel current session"""
|
||||
if self._current_session:
|
||||
logger.info("Cancelling RTL+ remote login session")
|
||||
self._current_session = None
|
||||
self._notifier.close(success=False, message="Cancelled")
|
||||
Reference in New Issue
Block a user