Prepare RTLPlus for new streaming prov

This commit is contained in:
Nirvana
2026-04-29 13:42:58 +02:00
parent c3fbce067b
commit acfe5e78c9
3 changed files with 217 additions and 198 deletions
@@ -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
@@ -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 = {
@@ -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."""