diff --git a/lib/streaming_providers/providers/rtlplus/auth.py b/lib/streaming_providers/providers/rtlplus/auth.py index 4c7bc8e..e3bc1f1 100644 --- a/lib/streaming_providers/providers/rtlplus/auth.py +++ b/lib/streaming_providers/providers/rtlplus/auth.py @@ -67,8 +67,8 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): @property def oauth_redirect_uri(self) -> str: - # Used in the authorize step - return f"{self.config.beta_website}tv-programm" + # Used in authorize step + return "https://beta.plus.rtl.de/tv-programm" @property def oauth_token_redirect_uri(self) -> str: @@ -177,17 +177,26 @@ class RTLPlusAuthenticator(BaseOAuth2Authenticator): self._oauth_state = fixed_state return fixed_state - def _build_token_exchange_payload( - self, authorization_code: str, code_verifier: str, state: str = None, **kwargs - ) -> Dict[str, Any]: + def _build_token_exchange_payload(self, authorization_code, code_verifier, state=None, **kwargs): + token_redirect = "https://beta.plus.rtl.de/silent-sso-iframe.html" + logger.debug(f"Token exchange redirect_uri: {token_redirect}") return { "grant_type": "authorization_code", "client_id": self.oauth_client_id, "code": authorization_code, - "redirect_uri": f"{self.config.beta_website}silent-sso-iframe.html", + "redirect_uri": token_redirect, "code_verifier": code_verifier, } + def _get_token_exchange_headers(self, **kwargs) -> Dict[str, str]: + headers = self._get_auth_headers() + headers.update({ + "Content-Type": "application/x-www-form-urlencoded", + "Origin": "https://beta.plus.rtl.de", + "Referer": "https://beta.plus.rtl.de/", + }) + return headers + def _get_client_id(self) -> str: if self._client_id: return self._client_id diff --git a/lib/streaming_providers/providers/rtlplus/channel_manager.py b/lib/streaming_providers/providers/rtlplus/channel_manager.py index b5ea013..7d0074c 100644 --- a/lib/streaming_providers/providers/rtlplus/channel_manager.py +++ b/lib/streaming_providers/providers/rtlplus/channel_manager.py @@ -12,11 +12,13 @@ Handles all linear TV (live channel) functionality: from typing import List, Optional, Dict, Any import json +import urllib.parse import time from datetime import date from ...base.models import Channel, DRMConfig, DRMSystem, LicenseConfig from ...base.utils.logger import logger +from .constants import RTLPlusDefaults class RTLPlusChannelManager: @@ -401,61 +403,107 @@ class RTLPlusChannelManager: preferred_quality: str = None, ) -> List[DRMConfig]: """ - Get DRM configuration for a linear TV channel (Widevine only for now). + Get DRM configuration for a linear TV channel. + Supports both Widevine and PlayReady. """ - # Normalize the channel identifier to SEO name channel_seo = self._normalize_channel_identifier(channel_id) - assets = self.extract_stream_assets(channel_seo) quality = preferred_quality or next(iter(self.cfg.preferred_qualities), "hd") - drm_configs = [] - # Process DASH (Widevine) - this is the primary DRM + # Process all assets for asset in assets: - if asset.get("format") == "dashcenc" and asset.get("quality") == quality: - drm_info = asset.get("drm", {}) - drm_config = drm_info.get("config", {}) - content_id = drm_config.get("contentId") + asset_format = asset.get("format", "") + asset_quality = asset.get("quality", "") - if not content_id: - logger.warning(f"No contentId in DRM config for {channel_seo}") + if asset_quality != quality: + continue + + drm_info = asset.get("drm", {}) + drm_config = drm_info.get("config", {}) + content_id = drm_config.get("contentId") + + if not content_id: + continue + + try: + uid = self.auth.get_user_id_from_token() + if not uid: + logger.error(f"No user ID available for DRM on {channel_seo}") continue - try: - uid = self.auth.get_user_id_from_token() - if not uid: - logger.error(f"No user ID available for DRM on {channel_seo}") - continue + # Get upfront token (same for both DRM systems) + upfront_token = self.auth.get_upfront_token( + content_id=content_id, + uid=uid, + ) - # Get upfront token - no extra parameters needed - upfront_token = self.auth.get_upfront_token( - content_id=content_id, - uid=uid, - ) + # Handle Widevine (DASH CENC) + if asset_format == "dashcenc": + wv_config = self._get_widevine_config(upfront_token) + if wv_config: + drm_configs.append(wv_config) + logger.debug(f"Added Widevine DRM for {channel_seo}") - license_url = self.cfg.drmtoday_license_url - headers = self.cfg.get_drm_license_headers(upfront_token) + # Handle PlayReady (hlsfp or playready format) + elif asset_format in ["hlsfp", "playready"]: + pr_config = self._get_playready_config(upfront_token) + if pr_config: + drm_configs.append(pr_config) + logger.debug(f"Added PlayReady DRM for {channel_seo}") - drm_configs.append(DRMConfig( - system=DRMSystem.WIDEVINE, - priority=3, - license=LicenseConfig( - server_url=license_url, - req_headers=json.dumps(headers), - req_data="{CHA-RAW}", - use_http_get_request=False, - ), - )) - logger.debug(f"Added Widevine DRM for {channel_seo}") - except Exception as e: - logger.error(f"Failed to get Widevine DRM for {channel_seo}: {e}") - break # Only need one DASH asset + except Exception as e: + logger.error(f"Failed to get DRM for {channel_seo}: {e}") + continue logger.info(f"Built {len(drm_configs)} DRM configs for {channel_seo}") return drm_configs + def _get_widevine_config(self, upfront_token: str) -> Optional[DRMConfig]: + """Get Widevine DRM configuration using existing headers from constants.""" + if not upfront_token: + return None + + # Use the existing method from RTLPlusConfig + headers = self.cfg.get_drm_license_headers(upfront_token) + + # URL-encode headers for req_headers parameter + req_headers = urllib.parse.urlencode(headers) + + return DRMConfig( + system=DRMSystem.WIDEVINE, + priority=3, + license=LicenseConfig( + server_url=self.cfg.drmtoday_license_url, + req_headers=req_headers, + req_data="{CHA-RAW}", + use_http_get_request=False, + ), + ) + + def _get_playready_config(self, upfront_token: str) -> Optional[DRMConfig]: + """Get PlayReady DRM configuration using existing headers from constants.""" + if not upfront_token: + return None + + # Use the existing method from RTLPlusConfig + headers = self.cfg.get_playready_license_headers(upfront_token) + + # URL-encode headers for req_headers parameter + req_headers = urllib.parse.urlencode(headers) + + return DRMConfig( + system=DRMSystem.PLAYREADY, + priority=2, # Slightly lower priority than Widevine (fallback) + license=LicenseConfig( + server_url=RTLPlusDefaults.DRMTODAY_PLAYREADY_URL, + req_headers=req_headers, + req_data="{CHA-RAW}", # PlayReady expects raw challenge + use_http_get_request=False, + ), + ) + # -------------------------------------------------------------------------- # Channel Info Helpers # -------------------------------------------------------------------------- diff --git a/lib/streaming_providers/providers/rtlplus/constants.py b/lib/streaming_providers/providers/rtlplus/constants.py index 85e684c..c69ba01 100644 --- a/lib/streaming_providers/providers/rtlplus/constants.py +++ b/lib/streaming_providers/providers/rtlplus/constants.py @@ -59,6 +59,7 @@ class RTLPlusDefaults: # DRM license server DRMTODAY_LICENSE_URL = "https://lic.drmtoday.com/license-proxy-widevine/cenc/" + DRMTODAY_PLAYREADY_URL = "https://lic.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx" # Direct CDN ORIGIN_CDN_BASE = "https://origin.live.rtlde.bedrock.tech" @@ -525,4 +526,9 @@ class RTLPlusConfig: return RTLPlusHeaders.get_playready_drm_headers( access_token=access_token, device_id=self.device_id, - ) \ No newline at end of file + ) + + @staticmethod + def get_playready_license_headers(upfront_token: str) -> dict: + """Get headers for PlayReady license request to DRMToday.""" + return RTLPlusHeaders.get_playready_drm_headers(upfront_token) \ No newline at end of file