diff --git a/lib/streaming_providers/providers/lib_drmtoday.py b/lib/streaming_providers/providers/lib_drmtoday.py new file mode 100644 index 0000000..f788fcf --- /dev/null +++ b/lib/streaming_providers/providers/lib_drmtoday.py @@ -0,0 +1,181 @@ +# streaming_providers/base/lib_drmtoday.py +""" +DRMToday License Server Integration + +Shared utilities for providers using DRMToday as their DRM license provider. +""" + +from typing import Optional +from ..base.models.drm import ( + DRMConfig, + DRMSystem, + LicenseConfig, + LicenseUnwrapperParams, +) + + +class DRMTodayConfig: + """Default DRMToday configuration constants""" + + # Default license server URLs + DEFAULT_WIDEVINE_URL = "https://lic.drmtoday.com/license-proxy-widevine/cenc/" + DEFAULT_PLAYREADY_URL = "https://lic.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx" + + # SOAP action for PlayReady + PLAYREADY_SOAP_ACTION = "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense" + + # Common unwrapper configuration for DRMToday + UNWRAPPER = "json,base64" + UNWRAPPER_PARAMS = {"path_data": "license"} + + +def create_drmtoday_widevine_config( + upfront_token: str, + origin: str, + referer: str, + user_agent: str, + license_url: Optional[str] = None, + priority: int = 3, +) -> DRMConfig: + """ + Create Widevine DRM configuration for DRMToday. + + Args: + upfront_token: DRMToday upfront token (x-dt-auth-token) + origin: Origin header value (e.g., "https://plus.rtl.de") + referer: Referer header value + user_agent: User-Agent string + license_url: Optional custom license URL (uses default if not provided) + priority: DRM priority (default 3) + + Returns: + DRMConfig ready for use in ISA + """ + license_url = license_url or DRMTodayConfig.DEFAULT_WIDEVINE_URL + + headers = { + "user-agent": user_agent, + "origin": origin, + "referer": referer, + "x-dt-auth-token": upfront_token, + } + + license_config = LicenseConfig( + server_url=license_url, + req_headers=headers, # LicenseConfig will normalize this to URL-encoded + req_data="{CHA-RAW}", + use_http_get_request=False, + unwrapper=DRMTodayConfig.UNWRAPPER, + unwrapper_params=LicenseUnwrapperParams(**DRMTodayConfig.UNWRAPPER_PARAMS), + ) + + return DRMConfig( + system=DRMSystem.WIDEVINE, + priority=priority, + license=license_config, + ) + + +def create_drmtoday_playready_config( + upfront_token: str, + origin: str, + referer: str, + user_agent: str, + license_url: Optional[str] = None, + priority: int = 2, +) -> DRMConfig: + """ + Create PlayReady DRM configuration for DRMToday. + + Args: + upfront_token: DRMToday upfront token (x-dt-auth-token) + origin: Origin header value (e.g., "https://plus.rtl.de") + referer: Referer header value + user_agent: User-Agent string (often Edge for PlayReady) + license_url: Optional custom license URL (uses default if not provided) + priority: DRM priority (default 2) + + Returns: + DRMConfig ready for use in ISA + """ + license_url = license_url or DRMTodayConfig.DEFAULT_PLAYREADY_URL + + headers = { + "Content-Type": "text/xml; charset=UTF-8", + "SOAPAction": DRMTodayConfig.PLAYREADY_SOAP_ACTION, + "User-Agent": user_agent, + "origin": origin, + "referer": referer, + "X-Dt-Auth-Token": upfront_token, + } + + license_config = LicenseConfig( + server_url=license_url, + req_headers=headers, + req_data="{CHA-RAW}", + use_http_get_request=False, + ) + + return DRMConfig( + system=DRMSystem.PLAYREADY, + priority=priority, + license=license_config, + ) + + +def create_drmtoday_configs( + upfront_token: str, + origin: str, + referer: str, + user_agent: str, + playready_user_agent: Optional[str] = None, + widevine_url: Optional[str] = None, + playready_url: Optional[str] = None, + widevine_priority: int = 3, + playready_priority: int = 2, +) -> list[DRMConfig]: + """ + Create both Widevine and PlayReady DRM configurations for DRMToday. + + This is the most common use case - return both DRM systems so ISA can choose. + + Args: + upfront_token: DRMToday upfront token + origin: Origin header value + referer: Referer header value + user_agent: User-Agent for Widevine requests + playready_user_agent: User-Agent for PlayReady (uses user_agent if None) + widevine_url: Optional custom Widevine license URL + playready_url: Optional custom PlayReady license URL + widevine_priority: Priority for Widevine (lower = higher priority) + playready_priority: Priority for PlayReady + + Returns: + List of DRMConfig objects (Widevine then PlayReady) + """ + drm_configs = [] + + # Widevine config + wv_config = create_drmtoday_widevine_config( + upfront_token=upfront_token, + origin=origin, + referer=referer, + user_agent=user_agent, + license_url=widevine_url, + priority=widevine_priority, + ) + drm_configs.append(wv_config) + + # PlayReady config (use provided UA or fallback to Widevine UA) + pr_user_agent = playready_user_agent or user_agent + pr_config = create_drmtoday_playready_config( + upfront_token=upfront_token, + origin=origin, + referer=referer, + user_agent=pr_user_agent, + license_url=playready_url, + priority=playready_priority, + ) + drm_configs.append(pr_config) + + return drm_configs \ No newline at end of file diff --git a/lib/streaming_providers/providers/rtlplus/constants.py b/lib/streaming_providers/providers/rtlplus/constants.py index ebb343c..4d4ef45 100644 --- a/lib/streaming_providers/providers/rtlplus/constants.py +++ b/lib/streaming_providers/providers/rtlplus/constants.py @@ -29,9 +29,6 @@ class RTLPlusDefaults: USER_AGENT = get_user_agent("windows", "chrome") PLAYREADY_USER_AGENT = get_user_agent("windows", "edge") - # PlayReady SOAP action - PLAYREADY_SOAP_ACTION = "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense" - # Supported platform identifiers PLATFORM_WEB = "web" PLATFORM_DEFAULT = PLATFORM_WEB @@ -60,14 +57,6 @@ class RTLPlusDefaults: PROFILES_PATH = "/v2/platforms/m6group_web/users/{user_id}/profiles" VERSION_ENDPOINT = "https://beta.plus.rtl.de/version.json" - # 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" - - # DRM License Response Parsing - DRM_LICENSE_UNWRAPPER = "json,base64" - DRM_LICENSE_UNWRAPPER_PARAMS = {"path_data": "license"} - # DRM Request Headers (common for all DRM types) DRM_COMMON_HEADERS = { "origin": BETA_WEBSITE.rstrip("/"), @@ -394,30 +383,6 @@ class RTLPlusHeaders: }) return headers - @staticmethod - def get_drm_license_headers(upfront_token: str, user_agent: str) -> dict: - """Headers for DRM license request to DRMToday.""" - headers = { - "content-type": "application/json", - "user-agent": user_agent, - "x-dt-auth-token": upfront_token, - } - # Add common DRM headers - headers.update(RTLPlusDefaults.DRM_COMMON_HEADERS) - return headers - - @staticmethod - def get_playready_license_headers(upfront_token: str, user_agent: str = None) -> dict: - """Get headers for PlayReady license request to DRMToday.""" - headers = { - "Content-Type": "text/xml; charset=UTF-8", - "SOAPAction": RTLPlusDefaults.PLAYREADY_SOAP_ACTION, - "User-Agent": user_agent or RTLPlusDefaults.PLAYREADY_USER_AGENT, - "X-Dt-Auth-Token": upfront_token, # Note: Different case for PlayReady? - } - headers.update(RTLPlusDefaults.DRM_COMMON_HEADERS) - return headers - @staticmethod def get_drm_headers(access_token: str, device_id: str = None, user_agent: str = None) -> dict: return { @@ -429,19 +394,6 @@ class RTLPlusHeaders: "X-Device-Name": RTLPlusDefaults.DEVICE_NAME, } - @staticmethod - def get_playready_drm_headers(access_token: str, device_id: str = None) -> dict: - return { - "Content-Type": "text/xml; charset=UTF-8", - "Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), - "Referer": RTLPlusDefaults.BASE_WEBSITE, - "SOAPAction": RTLPlusDefaults.PLAYREADY_SOAP_ACTION, - "User-Agent": RTLPlusDefaults.PLAYREADY_USER_AGENT, - "X-Auth-Token": access_token, - "X-Device-Id": device_id or RTLPlusDefaults.DEVICE_ID, - "X-Device-Name": RTLPlusDefaults.PLAYREADY_DEVICE_NAME, - } - @staticmethod def get_profiles_headers( oauth_token: str, @@ -497,9 +449,6 @@ class RTLPlusConfig: self.users_endpoint = config.get("users_endpoint", RTLPlusDefaults.USERS_ENDPOINT) self.profiles_path = config.get("profiles_path", RTLPlusDefaults.PROFILES_PATH) - # DRM license server - self.drmtoday_license_url = config.get("drmtoday_license_url", RTLPlusDefaults.DRMTODAY_LICENSE_URL) - # Quality presets for linear TV self.preferred_qualities = config.get("preferred_qualities", RTLPlusDefaults.LINEAR_TV_PREFERRED_QUALITIES) self.preferred_formats = config.get("preferred_formats", RTLPlusDefaults.LINEAR_TV_PREFERRED_FORMATS) @@ -588,38 +537,6 @@ class RTLPlusConfig: user_agent=self.user_agent, ) - def get_drm_license_headers(self, upfront_token: str) -> dict: - return RTLPlusHeaders.get_drm_license_headers( - upfront_token=upfront_token, - user_agent=self.user_agent, - ) - - def get_playready_license_headers(self, upfront_token: str) -> dict: - return RTLPlusHeaders.get_playready_license_headers( - upfront_token=upfront_token, - user_agent=self.user_agent, - ) - - @staticmethod - def get_drm_unwrapper_config() -> tuple: - """Get unwrapper configuration for DRM license responses.""" - return ( - RTLPlusDefaults.DRM_LICENSE_UNWRAPPER, - RTLPlusDefaults.DRM_LICENSE_UNWRAPPER_PARAMS, - ) - def get_drm_headers(self, access_token: str) -> dict: - return RTLPlusHeaders.get_drm_headers( - access_token=access_token, - device_id=self.device_id, - user_agent=self.user_agent, - ) - - def get_playready_drm_headers(self, access_token: str) -> dict: - return RTLPlusHeaders.get_playready_drm_headers( - access_token=access_token, - device_id=self.device_id, - ) - def get_layout_url(self, layout_type: str, content_id: str) -> str: """Get layout URL by type: 'live', 'video', 'folder', 'program', 'block'""" path_map = { diff --git a/lib/streaming_providers/providers/rtlplus/provider.py b/lib/streaming_providers/providers/rtlplus/provider.py index 83e3f21..38a491f 100644 --- a/lib/streaming_providers/providers/rtlplus/provider.py +++ b/lib/streaming_providers/providers/rtlplus/provider.py @@ -3,12 +3,11 @@ import json import time from datetime import datetime from typing import ClassVar, Dict, List, Optional, Tuple -import urllib.parse - +from ..lib_drmtoday import create_drmtoday_configs import requests -from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event, LicenseUnwrapperParams +from ...base.models import DRMConfig, StreamingChannel, Event from ...base.models.proxy_models import ProxyConfig from ...base.provider import StreamingProvider from ...base.utils import logger @@ -484,7 +483,23 @@ class RTLPlusProvider(StreamingProvider): logger.error(f"RTL+ Manifest Unexpected Error: {str(e)}") return None - # Add to RTLPlusProvider class + def _get_drm_vod_or_event(self, content_id: str, **kwargs) -> List[DRMConfig]: + """ + Get DRM configuration for VOD/event content using layout extraction. + """ + # Fetch layout for the content + layout = self.fetch_layout( + layout_type="video", + content_id=content_id, + location=f"{self.rtl_config.beta_website}{content_id}" + ) + + if not layout: + logger.error(f"Failed to fetch layout for VOD/event content_id: {content_id}") + return [] + + # Use the common DRM extraction method + return self.get_drm_for_content(layout) def get_drm_for_content(self, layout_data: Dict) -> List[DRMConfig]: """ @@ -492,12 +507,6 @@ class RTLPlusProvider(StreamingProvider): This method works for any layout type (live channel, event folder, VOD) that contains video assets with DRM configuration. - - Args: - layout_data: Layout JSON from fetch_layout() - - Returns: - List of DRMConfig objects """ assets = self.extract_video_assets(layout_data) @@ -562,19 +571,14 @@ class RTLPlusProvider(StreamingProvider): logger.error(f"Failed to get upfront token for {content_id}") return [] - drm_configs = [] - - # Build Widevine config - wv_config = self._get_widevine_config(upfront_token) - if wv_config: - drm_configs.append(wv_config) - logger.debug("Added Widevine DRM") - - # Build PlayReady config - pr_config = self._get_playready_config(upfront_token) - if pr_config: - drm_configs.append(pr_config) - logger.debug("Added PlayReady DRM") + # Use the shared DRMToday factory to create both configs + drm_configs = create_drmtoday_configs( + upfront_token=upfront_token, + origin=self.rtl_config.beta_website.rstrip("/"), + referer=self.rtl_config.beta_website, + user_agent=self.rtl_config.user_agent, + playready_user_agent=RTLPlusDefaults.PLAYREADY_USER_AGENT, # Edge for PlayReady + ) logger.info(f"Built {len(drm_configs)} DRM configs (Widevine + PlayReady)") return drm_configs @@ -583,55 +587,13 @@ class RTLPlusProvider(StreamingProvider): logger.error(f"Failed to get DRM: {e}") return [] - def _get_widevine_config(self, upfront_token: str) -> Optional[DRMConfig]: - """Get Widevine DRM configuration.""" - if not upfront_token: - return None - - headers = self.rtl_config.get_drm_license_headers(upfront_token) - req_headers = urllib.parse.urlencode(headers) - - license_config = LicenseConfig( - server_url=self.rtl_config.drmtoday_license_url, - req_headers=req_headers, - req_data="{CHA-RAW}", - use_http_get_request=False, - unwrapper="json,base64", - unwrapper_params=LicenseUnwrapperParams(path_data="license"), - ) - - return DRMConfig( - system=DRMSystem.WIDEVINE, - priority=3, - license=license_config, - ) - - def _get_playready_config(self, upfront_token: str) -> Optional[DRMConfig]: - """Get PlayReady DRM configuration.""" - if not upfront_token: - return None - - headers = self.rtl_config.get_playready_license_headers(upfront_token) - req_headers = urllib.parse.urlencode(headers) - - return DRMConfig( - system=DRMSystem.PLAYREADY, - priority=2, - license=LicenseConfig( - server_url=RTLPlusDefaults.DRMTODAY_PLAYREADY_URL, - req_headers=req_headers, - req_data="{CHA-RAW}", - use_http_get_request=False, - ), - ) - def get_drm(self, content_id: str, **kwargs) -> List[DRMConfig]: """ Get DRM configuration for content. Supports: - Linear TV channels (via channel_manager) - - VOD clips (via manifest extraction) + - VOD clips (via layout extraction) - Events (via event_manager) """ # Try linear TV channel first @@ -644,57 +606,16 @@ class RTLPlusProvider(StreamingProvider): if drm_configs: return drm_configs - # Fall back to VOD/event DRM extraction - return self._get_drm_vod_or_event(content_id, **kwargs) - - def _get_drm_vod_or_event(self, content_id: str, **kwargs) -> List[DRMConfig]: - """Original DRM logic for VOD/events.""" - try: - manifest_data = self._fetch_manifest_data(content_id) - if manifest_data is None: - return [] - - drm_configs = [] - access_token = self.authenticator.get_bearer_token() - - for stream in manifest_data: - if stream.get("name") == "dashhd" and "licenses" in stream: - licenses = stream.get("licenses", []) - - for license_info in licenses: - license_url = license_info.get("uri", {}).get("href") - if not license_url: - continue - - if license_info.get("type") == "WIDEVINE": - drm_configs.append(DRMConfig( - system=DRMSystem.WIDEVINE, - priority=3, - license=LicenseConfig( - server_url=license_url, - req_headers=json.dumps(self.rtl_config.get_drm_headers(access_token)), - req_data="{CHA-RAW}", - use_http_get_request=False, - ), - )) - elif license_info.get("type") == "PLAYREADY": - drm_configs.append(DRMConfig( - system=DRMSystem.PLAYREADY, - priority=2, - license=LicenseConfig( - server_url=license_url, - req_headers=json.dumps(self.rtl_config.get_playready_drm_headers(access_token)), - req_data="{CHA-RAW}", - use_http_get_request=False, - ), - )) - break - + # Fall back to VOD DRM extraction + # This will also handle VOD clips and any other content types + drm_configs = self._get_drm_vod_or_event(content_id, **kwargs) + if drm_configs: return drm_configs - except Exception as e: - logger.error(f"Error fetching DRM configs for RTL+ content {content_id}: {e}") - return [] + # If we get here, no DRM config could be found for any content type + logger.error( + f"No DRM configuration found for content_id: {content_id} (not a valid channel, event, or VOD item)") + return [] def _fetch_manifest_data(self, content_id: str) -> Optional[list]: """Fetch raw manifest data for VOD/events, with cache."""