Files
script.service.ultimate/lib/streaming_providers/providers/rtlplus/provider.py
T

424 lines
16 KiB
Python
Raw Normal View History

2025-10-29 20:23:50 +01:00
# lib/streaming_providers/providers/rtlplus/provider.py
import json
2026-01-06 16:41:03 +01:00
from typing import ClassVar, Dict, List, Optional
2025-10-29 20:23:50 +01:00
2025-12-23 16:51:49 +01:00
import requests
2026-01-06 16:41:03 +01:00
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
from ...base.models.proxy_models import ProxyConfig
2025-10-29 20:23:50 +01:00
from ...base.models.streaming_channel import StreamingChannel
2026-01-06 16:41:03 +01:00
from ...base.provider import StreamingProvider
2025-10-29 20:23:50 +01:00
from ...base.utils import logger
2026-01-06 16:41:03 +01:00
from .auth import RTLPlusAuthenticator
from .constants import RTLPlusConfig, RTLPlusDefaults
2025-10-29 20:23:50 +01:00
class RTLPlusProvider(StreamingProvider):
2025-12-23 16:51:49 +01:00
# Provider constants
2025-12-24 14:16:30 +01:00
PROVIDER_LABEL: ClassVar[str] = "RTL+"
2025-12-23 16:51:49 +01:00
PROVIDER_LOGO: ClassVar[str] = RTLPlusDefaults.RTLPLUS_LOGO
2026-01-06 16:41:03 +01:00
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = [
"client_credentials",
"user_credentials",
]
def __init__(
self,
country: str = "DE",
config: Optional[Dict] = None,
proxy_config: Optional[ProxyConfig] = None,
):
2025-10-29 20:23:50 +01:00
super().__init__(country)
2025-12-01 10:59:05 +01:00
# Initialize configuration
2025-10-29 20:23:50 +01:00
self.rtl_config = RTLPlusConfig(config)
self.channels_query_params = RTLPlusDefaults.CHANNELS_QUERY_PARAMS
2025-12-01 16:24:54 +01:00
# ✅ Using HTTP manager abstraction
2025-12-01 10:59:05 +01:00
self.http_manager = self._setup_http_manager(
2026-01-06 16:41:03 +01:00
provider_name="rtlplus",
2025-10-29 20:23:50 +01:00
proxy_config=proxy_config,
user_agent=self.rtl_config.user_agent,
2026-01-06 16:41:03 +01:00
timeout=self.rtl_config.timeout,
2025-10-29 20:23:50 +01:00
)
2025-12-01 10:59:05 +01:00
# Initialize authenticator
2025-12-01 16:24:54 +01:00
self.authenticator = RTLPlusAuthenticator(
2025-10-29 20:23:50 +01:00
client_version=self.rtl_config.client_version,
device_id=self.rtl_config.device_id,
proxy_config=proxy_config,
2026-01-06 16:41:03 +01:00
http_manager=self.http_manager,
2025-10-29 20:23:50 +01:00
)
2025-12-01 10:59:05 +01:00
# ✅ Share HTTP manager with authenticator
2026-01-16 12:24:41 +01:00
self.http_manager = self._share_http_manager_with_authenticator(self.authenticator)
2025-10-29 20:23:50 +01:00
2025-12-01 10:59:05 +01:00
# Try authentication
2025-10-29 20:23:50 +01:00
try:
2025-12-01 16:24:54 +01:00
self.bearer_token = self.authenticator.get_bearer_token()
2025-10-29 20:23:50 +01:00
logger.debug(f"RTL+ authentication successful during initialization")
except Exception as e:
logger.warning(f"RTL+ could not authenticate during initialization: {e}")
self.bearer_token = None
@property
def provider_name(self) -> str:
return "rtlplus"
2025-11-04 16:08:58 +01:00
@property
def provider_label(self) -> str:
2026-01-06 16:41:03 +01:00
return "RTL+"
2025-11-04 16:08:58 +01:00
2025-11-26 14:49:41 +01:00
@property
def provider_logo(self) -> str:
2025-12-23 16:51:49 +01:00
return self.PROVIDER_LOGO
2025-11-04 16:08:58 +01:00
2025-10-29 20:23:50 +01:00
@property
def uses_dynamic_manifests(self) -> bool:
# RTL+ provides relatively stable manifest URLs that can be fetched and cached
return False
2025-12-03 12:03:28 +01:00
@property
def implements_epg(self) -> bool:
return False
2025-12-19 23:17:20 +01:00
@property
def supported_auth_types(self) -> List[str]:
2026-01-06 16:41:03 +01:00
return ["user_credentials"]
2025-12-19 23:17:20 +01:00
2025-12-23 16:51:49 +01:00
# ============================================================================
2025-12-01 16:24:54 +01:00
# OPTION 1: Provider-specific method (RECOMMENDED - No signature conflict)
# ============================================================================
def _get_rtlplus_authenticated_headers(self) -> Dict[str, str]:
2025-10-29 20:23:50 +01:00
"""
Get headers with authentication and RTL+ specific headers
2025-12-01 16:24:54 +01:00
This will automatically upgrade from anonymous to user token if possible
2025-10-29 20:23:50 +01:00
"""
2025-12-01 16:24:54 +01:00
bearer_token = self.authenticator.get_bearer_token(force_upgrade=True)
2025-10-29 20:23:50 +01:00
return self.rtl_config.get_api_headers(access_token=bearer_token)
2025-11-22 13:08:03 +01:00
def get_channels(self, **kwargs) -> List[StreamingChannel]:
2025-10-29 20:23:50 +01:00
"""
Fetch channels from RTL+ GraphQL API with authentication
"""
try:
2025-12-01 16:24:54 +01:00
# ✅ Use provider-specific method
headers = self._get_rtlplus_authenticated_headers()
2025-10-29 20:23:50 +01:00
response = self.http_manager.get(
self.rtl_config.graphql_endpoint,
2026-01-06 16:41:03 +01:00
operation="api",
2025-10-29 20:23:50 +01:00
params=self.channels_query_params,
2026-01-06 16:41:03 +01:00
headers=headers,
2025-10-29 20:23:50 +01:00
)
response.raise_for_status()
data = response.json()
channels = []
2026-01-06 16:41:03 +01:00
if "data" in data and "liveTvStations" in data["data"]:
for station in data["data"]["liveTvStations"]:
2025-10-29 20:23:50 +01:00
channel = self._parse_station_to_channel(station)
if channel:
channels.append(channel)
self.channels = channels
return channels
except requests.RequestException as e:
2025-12-01 16:24:54 +01:00
logger.error(f"Error fetching RTL+ channels: {e}")
2025-10-29 20:23:50 +01:00
# Try to refresh auth token and retry once
try:
2025-12-01 16:24:54 +01:00
logger.info("Attempting to refresh authentication and retry...")
self.authenticator.invalidate_token()
headers = self._get_rtlplus_authenticated_headers()
2025-10-29 20:23:50 +01:00
response = self.http_manager.get(
self.rtl_config.graphql_endpoint,
2026-01-06 16:41:03 +01:00
operation="api",
2025-10-29 20:23:50 +01:00
params=self.channels_query_params,
2026-01-06 16:41:03 +01:00
headers=headers,
2025-10-29 20:23:50 +01:00
)
response.raise_for_status()
data = response.json()
channels = []
2026-01-06 16:41:03 +01:00
if "data" in data and "liveTvStations" in data["data"]:
for station in data["data"]["liveTvStations"]:
2025-10-29 20:23:50 +01:00
channel = self._parse_station_to_channel(station)
if channel:
channels.append(channel)
self.channels = channels
return channels
except Exception as retry_e:
2025-12-01 16:24:54 +01:00
logger.error(f"Retry failed: {retry_e}")
2025-10-29 20:23:50 +01:00
return []
except Exception as e:
2025-12-01 16:24:54 +01:00
logger.error(f"Error parsing RTL+ channels: {e}")
2025-10-29 20:23:50 +01:00
return []
def _parse_station_to_channel(self, station: Dict) -> Optional[StreamingChannel]:
"""
Parse a station object from RTL+ API to StreamingChannel
"""
try:
# Extract basic info
2026-01-06 16:41:03 +01:00
name = station.get("name", "")
channel_id = station.get("id", "")
2025-10-29 20:23:50 +01:00
if not name or not channel_id:
return None
# Extract logo URL
logo_url = None
2026-01-06 16:41:03 +01:00
if "images" in station and "alternativeLandscapeUri" in station["images"]:
logo_url = station["images"]["alternativeLandscapeUri"]
2025-10-29 20:23:50 +01:00
# Determine if premium channel
2026-01-06 16:41:03 +01:00
is_premium = station.get("isPremium", False)
2025-10-29 20:23:50 +01:00
# Extract watch path for potential manifest fetching
watch_path = None
2026-01-06 16:41:03 +01:00
if "urlData" in station and "watchPath" in station["urlData"]:
watch_path = station["urlData"]["watchPath"]
2025-10-29 20:23:50 +01:00
# Create channel object
channel = StreamingChannel(
name=name,
channel_id=channel_id,
provider=self.provider_name,
logo_url=logo_url,
mode="live",
session_manifest=True, # RTL+ uses dynamic manifests
manifest=None, # Will be set dynamically
manifest_script=watch_path, # Store watch path for manifest fetching
content_type="LIVE",
country=self.country,
2026-01-06 16:41:03 +01:00
language="de",
2025-10-29 20:23:50 +01:00
)
# Set CDM settings for premium channels
if is_premium:
channel.use_cdm = True
2025-12-01 16:24:54 +01:00
channel.cdm_type = "widevine"
2025-10-29 20:23:50 +01:00
return channel
except Exception as e:
2025-12-01 16:24:54 +01:00
logger.warning(f"Error parsing station {station}: {e}")
2025-10-29 20:23:50 +01:00
return None
2026-01-06 16:41:03 +01:00
def enrich_channel_data(
self, channel: StreamingChannel, **kwargs
) -> Optional[StreamingChannel]:
2025-10-29 20:23:50 +01:00
"""
Enrich channel with manifest URL and additional data
"""
try:
# Fetch manifest URL for this channel
manifest_url = self.get_manifest(channel.channel_id, **kwargs)
if manifest_url:
# Set the manifest URL - RTL+ provides relatively stable URLs
channel.set_static_manifest(manifest_url)
# Check if this channel has DRM
2025-11-22 13:08:03 +01:00
drm_configs = self.get_drm(channel.channel_id, **kwargs)
2025-10-29 20:23:50 +01:00
if drm_configs:
# Set DRM configuration
channel.use_cdm = True
channel.cdm_type = "widevine" # Default to Widevine
# Set license URL from first Widevine config
for config in drm_configs:
2025-12-01 16:24:54 +01:00
if config.system == DRMSystem.WIDEVINE:
channel.license_url = config.license.server_url
2025-10-29 20:23:50 +01:00
break
else:
channel.use_cdm = False
channel.cdm_type = None
return channel
else:
2026-01-06 16:41:03 +01:00
logger.warning(
f"Could not fetch manifest for channel {channel.name} ({channel.channel_id})"
)
2025-10-29 20:23:50 +01:00
return channel
except Exception as e:
2025-12-01 16:24:54 +01:00
logger.error(f"Error enriching channel data for {channel.name}: {e}")
2025-10-29 20:23:50 +01:00
return channel
def get_manifest(self, channel_id: str, **kwargs) -> Optional[str]:
manifest_url = self.rtl_config.get_manifest_url(channel_id)
try:
logger.debug(f"RTL+ Manifest Request: GET {manifest_url}")
headers = self.rtl_config.get_base_headers()
2026-01-16 12:24:41 +01:00
response = self.http_manager.get(manifest_url, operation="manifest", headers=headers)
2025-10-29 20:23:50 +01:00
logger.debug(f"RTL+ Manifest Response: Status={response.status_code}")
logger.debug(f"RTL+ Response Headers: {dict(response.headers)}")
response.raise_for_status()
manifest_data = response.json()
2026-01-16 12:24:41 +01:00
logger.debug(f"RTL+ Manifest Data: {self._sanitize_manifest_log(manifest_data)}")
2025-10-29 20:23:50 +01:00
2025-12-01 16:24:54 +01:00
# Process manifest data
2026-01-06 16:41:03 +01:00
quality_preference = ["dashhd", "dashsd"]
2025-10-29 20:23:50 +01:00
for quality in quality_preference:
for stream in manifest_data:
2026-01-06 16:41:03 +01:00
if stream.get("name") == quality:
sources = stream.get("sources", [])
2026-01-16 12:24:41 +01:00
non_yospace_sources = [s for s in sources if not s.get("isYospace", False)]
2025-10-29 20:23:50 +01:00
if non_yospace_sources:
2026-01-06 16:41:03 +01:00
selected_url = non_yospace_sources[0].get("url")
2025-10-29 20:23:50 +01:00
logger.info(f"RTL+ Selected Manifest URL: {selected_url}")
return selected_url
# Fallback logic
for stream in manifest_data:
2026-01-06 16:41:03 +01:00
sources = stream.get("sources", [])
2025-10-29 20:23:50 +01:00
if sources:
2026-01-06 16:41:03 +01:00
fallback_url = sources[0].get("url")
2025-10-29 20:23:50 +01:00
logger.info(f"RTL+ Using Fallback Manifest URL: {fallback_url}")
return fallback_url
logger.warning("RTL+ No valid manifest URL found in response")
return None
except requests.RequestException as e:
logger.error(f"RTL+ Manifest HTTP Error: {str(e)}")
return None
except json.JSONDecodeError as e:
logger.error(f"RTL+ Manifest JSON Parse Error: {str(e)}")
return None
except Exception as e:
logger.error(f"RTL+ Manifest Unexpected Error: {str(e)}")
return None
@staticmethod
def _sanitize_manifest_log(manifest_data: Dict) -> Dict:
"""
Sanitize manifest data for logging (remove sensitive information)
"""
try:
# Create a copy to avoid modifying original
sanitized = manifest_data.copy()
# Remove or truncate potentially sensitive URLs
if isinstance(sanitized, list):
for stream in sanitized:
if isinstance(stream, dict):
2026-01-06 16:41:03 +01:00
if "sources" in stream:
for source in stream["sources"]:
if "url" in source:
2025-10-29 20:23:50 +01:00
# Truncate long URLs for logging
2026-01-06 16:41:03 +01:00
url = source["url"]
2025-10-29 20:23:50 +01:00
if len(url) > 100:
2026-01-06 16:41:03 +01:00
source["url"] = url[:100] + "..."
2025-10-29 20:23:50 +01:00
return sanitized
except Exception:
return manifest_data
2025-11-22 13:08:03 +01:00
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
2025-10-29 20:23:50 +01:00
"""
Get DRM configurations for a channel from RTL+ streaming API
"""
try:
# Fetch manifest data to get license information
manifest_url = self.rtl_config.get_manifest_url(channel_id)
2026-01-06 16:41:03 +01:00
response = self.http_manager.get(manifest_url, operation="manifest")
2025-10-29 20:23:50 +01:00
response.raise_for_status()
manifest_data = response.json()
drm_configs = []
# Get access token for license requests
2025-12-01 16:24:54 +01:00
access_token = self.authenticator.get_bearer_token()
2025-10-29 20:23:50 +01:00
# Look for dashhd streams (preferred quality) and extract DRM info
for stream in manifest_data:
2026-01-06 16:41:03 +01:00
if stream.get("name") == "dashhd" and "licenses" in stream:
licenses = stream.get("licenses", [])
2025-10-29 20:23:50 +01:00
for license_info in licenses:
2026-01-06 16:41:03 +01:00
license_url = license_info.get("uri", {}).get("href")
2025-10-29 20:23:50 +01:00
if not license_url:
continue
2026-01-16 12:24:41 +01:00
def create_drm_config(drm_system, priority, server_url, headers):
2025-12-01 16:24:54 +01:00
return DRMConfig(
system=drm_system,
priority=priority,
2025-10-29 20:23:50 +01:00
license=LicenseConfig(
2025-12-01 16:24:54 +01:00
server_url=server_url,
req_headers=json.dumps(headers),
2025-10-29 20:23:50 +01:00
req_data="{CHA-RAW}",
2026-01-06 16:41:03 +01:00
use_http_get_request=False,
),
2025-10-29 20:23:50 +01:00
)
2026-01-06 16:41:03 +01:00
if license_info.get("type") == "WIDEVINE":
drm_configs.append(
create_drm_config(
DRMSystem.WIDEVINE,
1,
license_url,
self.rtl_config.get_drm_headers(access_token),
)
)
elif license_info.get("type") == "PLAYREADY":
drm_configs.append(
create_drm_config(
DRMSystem.PLAYREADY,
2,
license_url,
self.rtl_config.get_drm_headers(access_token),
)
)
elif license_info.get("type") == "FAIRPLAY":
drm_configs.append(
create_drm_config(
DRMSystem.FAIRPLAY,
3,
license_url,
self.rtl_config.get_drm_headers(access_token),
)
)
2025-10-29 20:23:50 +01:00
break
return drm_configs
except requests.RequestException as e:
2026-01-16 12:24:41 +01:00
logger.error(f"Error fetching DRM configs for RTL+ channel {channel_id}: {e}")
2025-10-29 20:23:50 +01:00
return []
except Exception as e:
2026-01-16 12:24:41 +01:00
logger.error(f"Error parsing DRM configs for RTL+ channel {channel_id}: {e}")
2025-10-29 20:23:50 +01:00
return []
@staticmethod
def get_epg_data(channel_id: str, **kwargs) -> Optional[Dict]:
"""
Get EPG data for a channel
"""
# RTL+ EPG implementation would go here
# This is a placeholder for future implementation
return None
def get_license_url(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
"""
Get license URL for a DRM-protected channel
"""
2025-11-22 13:08:03 +01:00
drm_configs = self.get_drm(channel.channel_id, **kwargs)
2025-10-29 20:23:50 +01:00
if drm_configs:
# Return the first license URL found
return drm_configs[0].license.server_url
2026-01-06 16:41:03 +01:00
return None