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

610 lines
25 KiB
Python
Raw Normal View History

2025-10-29 20:23:50 +01:00
# streaming_providers/providers/rtlplus/constants.py
"""
RTL+ provider constants and default configurations
"""
2026-03-10 16:19:50 +01:00
from ..globals import get_user_agent
import json
import urllib.request
2026-04-23 20:55:52 +02:00
from typing import Optional
2026-03-10 16:19:50 +01:00
from ...base.utils.logger import logger
2025-10-29 20:23:50 +01:00
class RTLPlusDefaults:
"""Default values for RTL+ provider"""
2026-01-06 16:41:03 +01:00
2026-04-29 12:33:55 +02:00
RTLPLUS_LOGO = "https://upload.wikimedia.org/wikipedia/commons/0/0f/RTL_DE_Logo_2021_pink_blau_gruen.png"
2025-10-29 20:23:50 +01:00
# Device information
2026-04-30 15:27:37 +02:00
# DEVICE_ID = "8c3f37cc-13a3-4141-bd0f-e4b3673fe5e4"
DEVICE_ID = "_luid_f4f52150-0645-43f8-a8f5-029341f49980"
2026-01-06 16:41:03 +01:00
DEVICE_NAME = "Linux Chrome"
2026-02-27 21:09:54 +01:00
PLAYREADY_DEVICE_NAME = "Windows Edge"
2025-10-29 20:23:50 +01:00
2026-04-23 22:36:36 +02:00
# Device screen size (used in headers)
2026-04-27 13:12:36 +02:00
DEVICE_PLAYER_SIZE_WIDTH = 3840
DEVICE_PLAYER_SIZE_HEIGHT = 2160
2026-04-23 22:36:36 +02:00
2026-04-23 20:55:52 +02:00
# User agents
2026-03-10 16:19:50 +01:00
USER_AGENT = get_user_agent("windows", "chrome")
PLAYREADY_USER_AGENT = get_user_agent("windows", "edge")
2026-02-27 18:37:16 +01:00
2026-03-09 11:41:57 +01:00
# Supported platform identifiers
PLATFORM_WEB = "web"
2026-03-10 16:51:18 +01:00
PLATFORM_DEFAULT = PLATFORM_WEB
2026-03-10 16:19:50 +01:00
# Client and version information
2026-04-23 20:55:52 +02:00
CLIENT_VERSION_FALLBACK = "2025.6.26.0"
2026-03-10 16:19:50 +01:00
CLIENT_ID = f"rtlplus-{PLATFORM_DEFAULT}"
2026-04-24 13:11:10 +02:00
BEDROCK_CLIENT_ID = "bedrock-m6group_web"
2026-08-14 19:28:42 +02:00
# 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"
2026-03-09 11:41:57 +01:00
2025-10-29 20:23:50 +01:00
# API endpoints
2026-05-29 10:02:47 +02:00
AUTH_REALM_BASE = "https://auth.rtl.de/auth/realms/rtlplus"
AUTH_BASE_URL = f"{AUTH_REALM_BASE}/protocol/openid-connect"
2026-01-06 16:41:03 +01:00
AUTH_ENDPOINT = f"{AUTH_BASE_URL}/token"
2026-08-14 19:28:42 +02:00
# 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"
2026-01-06 16:41:03 +01:00
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"
2025-10-29 20:23:50 +01:00
2026-04-23 20:55:52 +02:00
# Bedrock API endpoints (Linear TV)
BEDROCK_AUTH_URL = "https://front-auth.rtlde.bedrock.tech/v2/rtlde/platforms/m6group_web/token"
BEDROCK_LAYOUT_BASE = "https://layout.rtlde.bedrock.tech/front/v1/rtlde/m6group_web/main/token-web-31"
BEDROCK_DRM_UPFRONT_BASE = "https://drm.rtlde.bedrock.tech/v1/customers/rtlde/platforms/m6group_web/services/rtlplus_root/users/{uid}/live"
BEDROCK_HEARTBEAT_URL = "https://heartbeat-v2.rtlde.bedrock.tech/v2/platforms/m6group_web/notify/session_live"
2026-04-23 22:36:36 +02:00
TIME_ENDPOINT = "https://time.rtlde.bedrock.tech/"
2026-04-27 11:01:52 +02:00
USERS_ENDPOINT = "https://users.rtlde.bedrock.tech"
PROFILES_PATH = "/v2/platforms/m6group_web/users/{user_id}/profiles"
2026-04-28 18:54:26 +02:00
VERSION_ENDPOINT = "https://beta.plus.rtl.de/version.json"
2026-04-23 20:55:52 +02:00
2026-04-27 13:09:05 +02:00
# DRM Request Headers (common for all DRM types)
DRM_COMMON_HEADERS = {
2026-05-05 10:18:50 +02:00
"origin": BASE_WEBSITE.rstrip("/"),
"referer": BASE_WEBSITE,
2026-04-27 13:09:05 +02:00
}
2026-04-23 20:55:52 +02:00
# Direct CDN
ORIGIN_CDN_BASE = "https://origin.live.rtlde.bedrock.tech"
2026-05-03 21:16:51 +02:00
# Image CDN
IMAGE_BASE_URL = "https://images-fio.rtlde.bedrock.tech/v2/images"
IMAGE_PARAMS = "auto=webp&blur=0&fit=scale_crop&height=320&interlace=1&optimize=high&width=213"
2026-05-18 10:53:45 +02:00
IMAGE_PARAMS_LOGO = "auto=avif&blur=0&fit=scale_crop&height=1080&interlace=1&optimize=high&width=1080"
2026-05-03 22:01:29 +02:00
IMAGE_SIGNING_KEY = "x9vGg4RNeNBqV2nBfhqLV6cN4n"
2026-05-03 21:16:51 +02:00
2025-10-29 20:23:50 +01:00
# Anonymous credentials (fallback)
2026-01-06 16:41:03 +01:00
ANONYMOUS_CLIENT_ID = "anonymous-user"
ANONYMOUS_CLIENT_SECRET = "4bfeb73f-1c4a-4e9f-a7fa-96aa1ad3d94c"
2025-10-29 20:23:50 +01:00
# HTTP settings
DEFAULT_TIMEOUT = 30
2026-04-23 20:55:52 +02:00
# Custom header carrying the signed profile JWT
2026-03-07 15:19:07 +01:00
PROFILE_HEADER = "Rtlplus-Profile"
# requiredPermission values returned by the events API
PERMISSION_FREE_TV = "liveeventAccessToFreeTv"
PERMISSION_PAY_TV = "liveeventAccessToPayTv"
# GraphQL __typename values used during response parsing
TYPENAME_LIVE_EVENT_WIDGET = "LiveEventWidget"
TYPENAME_LIVE_EVENT = "LiveEvent"
2026-04-23 20:55:52 +02:00
# Linear TV Quality Presets
LINEAR_TV_PREFERRED_QUALITIES = ["hd", "sd"]
LINEAR_TV_PREFERRED_FORMATS = ["dashcenc", "hlsfp"]
2026-06-06 22:48:16 +02:00
# Deprecated: DRM type ordering is now handled by the scoring system in
# extract_best_manifest_url / get_drm_for_content. This list is retained
# only for backward-compatibility with existing config dicts.
2026-04-23 20:55:52 +02:00
LINEAR_TV_PREFERRED_DRM_TYPES = ["hardware", "software"]
2026-06-06 22:48:16 +02:00
# DRM variant selection
DRM_VARIANT_AUTO = "auto" # Prefer hardware DRM, fall back to software
DRM_VARIANT_SOFTWARE = "software" # Force software DRM only
DRM_VARIANT_HARDWARE = "hardware" # Force hardware DRM only
DEFAULT_DRM_VARIANT = DRM_VARIANT_AUTO
# All valid DRM variant values (used for input validation)
VALID_DRM_VARIANTS = frozenset([DRM_VARIANT_AUTO, DRM_VARIANT_SOFTWARE, DRM_VARIANT_HARDWARE])
2026-04-28 18:54:26 +02:00
# Layout path patterns
LAYOUT_PATH_LIVE = "/live/{id}/layout"
LAYOUT_PATH_VIDEO = "/video/{id}/layout"
LAYOUT_PATH_FOLDER = "/folder/{id}/layout"
LAYOUT_PATH_PROGRAM = "/program/{id}/layout"
LAYOUT_PATH_BLOCK = "/block/{id}"
# Default pagination values
DEFAULT_BLOCK_PAGE = 1
DEFAULT_NB_PAGES = 2
DEFAULT_BLOCK_NB_PAGES = 3
# Cache TTL for layouts (5 minutes)
LAYOUT_CACHE_TTL = 300
2026-03-09 17:22:42 +01:00
# ---------------------------------------------------------------------------
# VOD — stream config endpoints
# ---------------------------------------------------------------------------
VOD_PREFERRED_VARIANTS = ["dashhd", "dashsd", "hlsfairplayhd", "hlsfairplaysd"]
2026-03-10 16:19:50 +01:00
VOD_PLATFORM_GRAPHQL = PLATFORM_DEFAULT.upper()
2026-03-09 17:22:42 +01:00
VOD_WURSTLAND_CONFIG_URL = "https://wurstland.plus.rtl.de/config/{rrn}/{platform}"
2026-04-23 20:55:52 +02:00
VOD_WURSTLAND_PLATFORM = PLATFORM_DEFAULT.upper()
2026-03-09 17:22:42 +01:00
# ---------------------------------------------------------------------------
# VOD — root category definition
# ---------------------------------------------------------------------------
VOD_ROOT_SLUG = "topic-worlds"
2026-04-23 20:55:52 +02:00
VOD_MOVIES_ROOT_WATCH_PATH = "/video-tv/filme"
2026-03-09 20:37:37 +01:00
VOD_MOVIES_GENRE_WATCH_PATH = "/video-tv/filme/genre"
2026-04-23 20:55:52 +02:00
VOD_MOVIES_ROOT_ID = "/video-tv/filme"
2026-03-09 20:37:37 +01:00
VOD_MOVIE_GENRE_SLUGS = [
"action", "abenteuer", "animation", "comedy", "dokumentation",
"drama", "fantasy", "horror", "kinder", "krimi",
"liebesfilm", "science-fiction", "thriller",
]
2026-04-23 20:55:52 +02:00
VOD_SERIES_ROOT_WATCH_PATH = "/video-tv/serien"
2026-03-09 20:37:37 +01:00
VOD_SERIES_GENRE_WATCH_PATH = "/video-tv/serien/genre"
2026-04-23 20:55:52 +02:00
VOD_SERIES_ROOT_ID = "/video-tv/serien"
2026-03-09 20:37:37 +01:00
VOD_SERIES_GENRE_SLUGS = [
"action", "animation", "comedy", "crime", "dokumentation",
"drama", "fantasy", "horror", "kinder", "reality",
"romance", "science-fiction", "thriller",
]
2026-04-23 20:55:52 +02:00
VOD_SHOWS_ROOT_WATCH_PATH = "/video-tv/shows"
2026-03-10 12:37:57 +01:00
VOD_SHOWS_GENRE_WATCH_PATH = "/video-tv/shows/genre"
2026-04-23 20:55:52 +02:00
VOD_SHOWS_ROOT_ID = "/video-tv/shows"
2026-03-10 12:37:57 +01:00
VOD_SHOW_GENRE_SLUGS = [
"action", "comedy", "crime", "dokumentation",
"drama", "kinder", "reality", "romance", "thriller",
]
2026-04-23 20:55:52 +02:00
VOD_ELEMENT_TYPE_MOVIE = "MOVIE"
2026-03-09 20:37:37 +01:00
VOD_ELEMENT_TYPE_SERIES = "SERIES"
2026-04-23 20:55:52 +02:00
VOD_ELEMENT_TYPE_SHOW = "SHOW"
2026-03-09 20:37:37 +01:00
VOD_OVERVIEW_PAGE_LIMIT = 48
2026-03-09 17:22:42 +01:00
2026-04-23 20:55:52 +02:00
# Lazy-loaded client version
_CLIENT_VERSION_CACHE: Optional[str] = None
2026-03-10 10:04:03 +01:00
2026-04-23 20:55:52 +02:00
def _get_rtlplus_client_version() -> str:
2026-04-28 18:54:26 +02:00
"""Lazy fetch RTL+ client version from version.json endpoint."""
2026-04-23 20:55:52 +02:00
global _CLIENT_VERSION_CACHE
if _CLIENT_VERSION_CACHE is None:
try:
2026-04-28 18:54:26 +02:00
with urllib.request.urlopen(RTLPlusDefaults.VERSION_ENDPOINT, timeout=4) as resp:
2026-04-23 20:55:52 +02:00
data = json.loads(resp.read().decode())
_CLIENT_VERSION_CACHE = data["version"]
except Exception as exc:
2026-04-23 22:36:36 +02:00
logger.warning(
f"Could not fetch RTL+ client version ({exc}); using fallback {RTLPlusDefaults.CLIENT_VERSION_FALLBACK}")
2026-04-23 20:55:52 +02:00
_CLIENT_VERSION_CACHE = RTLPlusDefaults.CLIENT_VERSION_FALLBACK
return _CLIENT_VERSION_CACHE
class RTLPlusGraphQL:
"""Persisted-query hashes and parameter builders for all RTL+ GraphQL operations."""
2026-03-10 16:19:50 +01:00
2026-04-23 20:55:52 +02:00
HASHES: dict[str, str] = {
"LiveTvStations": "845cf56a2a78110a0f978c1a2af2bc7f9a1c937d0f324ffaf852a9a4414c8485",
2026-03-10 10:04:03 +01:00
"ExploreWidgetWatch": "724f21ab86aa3f8c57673a3b346cf119f6a155bf82bb0201fd3b18e28e44f1ed",
2026-04-23 20:55:52 +02:00
"TopicWorlds": "3dbde4c45532f4bdb0a1d7c210db43f0888f8a82f4aab43f7a90f3c4762d8ff7",
"Format": "d112638c0184ab5698af7b69532dfe2f12973f7af9cb137b9f70278130b1eafa",
"MRE": "0c77404637570adff548e329a48654498c54ce1c36a459d72586ff18999bebaa",
"Episode": "87dbde15a0d269b11606f5ff458d555e98eb493bb4fb6ddc150d812d5e9a9cf8",
"WatchPlayerConfigV3": "fea0311fb572b6fded60c5a1a9d652f97f55d182bc4cedbdad676354a8d2797c",
"SeoUrlData": "fcc4a812d6b93496f00c3068234db7722f553032bb760e09e5e6c74586c86f8d",
"OverviewPage": "28aad4e992bb63330bfcd40a6906af3119d8a2612fa9fd28dae9c19127e247ca",
"LiveEventsOverviewPage": "77a8f26d5de76daf801fcd7ae54bebd0ecabbeeb3ecb90889bc4a2e5590b7d20",
"SeasonWithFormatAndEpisodes": "cc0fbbe17143f549a35efa6f8665ceb9b1cfae44b590f0b2381a9a304304c584",
2026-03-10 10:04:03 +01:00
}
2026-03-09 17:22:42 +01:00
2026-03-10 10:04:03 +01:00
@classmethod
def _build(cls, operation: str, variables: str) -> dict:
return {
"operationName": operation,
"variables": variables,
2026-04-23 20:55:52 +02:00
"extensions": f'{{"persistedQuery":{{"version":1,"sha256Hash":"{cls.HASHES[operation]}"}}}}',
2026-03-10 10:04:03 +01:00
}
2026-03-09 17:22:42 +01:00
2026-03-10 10:04:03 +01:00
@classmethod
def live_tv_stations(cls, epg_count: int = 4) -> dict:
2026-04-23 22:36:36 +02:00
return cls._build("LiveTvStations",
f'{{"epgCount":{epg_count},"filter":{{"channelTypes":["BROADCAST","FAST"]}}}}')
2026-03-09 17:22:42 +01:00
2026-03-10 10:04:03 +01:00
@classmethod
def explore_widget_watch(cls, area: str = "home", offset: int = 0, take: int = 15) -> dict:
2026-04-23 20:55:52 +02:00
return cls._build("ExploreWidgetWatch", f'{{"area":"{area}","offset":{offset},"take":{take}}}')
2026-03-09 17:22:42 +01:00
2026-03-10 13:04:11 +01:00
@classmethod
def season_with_episodes(cls, season_rrn: str, offset: int = 0, limit: int = 48) -> dict:
2026-04-23 22:36:36 +02:00
return cls._build("SeasonWithFormatAndEpisodes",
f'{{"seasonId":"{season_rrn}","offset":{offset},"limit":{limit}}}')
2026-03-10 13:04:11 +01:00
2026-03-10 12:37:57 +01:00
@classmethod
def live_events_overview_page(cls) -> dict:
2026-03-10 13:04:11 +01:00
return cls._build("LiveEventsOverviewPage", "{}")
2026-03-10 12:37:57 +01:00
2026-03-10 10:04:03 +01:00
@classmethod
def topic_worlds(cls, take: int = 100, offset: int = 0) -> dict:
2026-04-23 20:55:52 +02:00
return cls._build("TopicWorlds", f'{{"take":{take},"offset":{offset},"filterForSearchGrid":true}}')
2026-03-10 10:04:03 +01:00
@classmethod
def format(cls, format_rrn: str) -> dict:
return cls._build("Format", f'{{"id":"{format_rrn}"}}')
@classmethod
def mre(cls, format_rrn: str) -> dict:
return cls._build("MRE", f'{{"id":"{format_rrn}"}}')
@classmethod
def episode(cls, episode_rrn: str) -> dict:
return cls._build("Episode", f'{{"rrn":"{episode_rrn}"}}')
@classmethod
def watch_player_config(cls, rrn: str) -> dict:
2026-04-23 22:36:36 +02:00
return cls._build("WatchPlayerConfigV3",
f'{{"platform":"{RTLPlusDefaults.VOD_PLATFORM_GRAPHQL}","id":"{rrn}"}}')
2026-03-10 10:04:03 +01:00
@classmethod
def seo_url_data(cls, watch_path: str) -> dict:
return cls._build("SeoUrlData", f'{{"watchPath":"{watch_path}"}}')
@classmethod
2026-04-23 20:55:52 +02:00
def overview_page(cls, element_type: str, genres: list = None, offset: int = 0, limit: int = None) -> dict:
2026-03-10 10:04:03 +01:00
if limit is None:
limit = RTLPlusDefaults.VOD_OVERVIEW_PAGE_LIMIT
filter_parts = [f'"elementType":"{element_type}"']
if genres:
genres_json = "[" + ",".join(f'"{g}"' for g in genres) + "]"
filter_parts.append(f'"genres":{genres_json}')
2026-04-23 20:55:52 +02:00
variables = f'{{"pagination":{{"offset":{offset},"limit":{limit}}},"filter":{{{",".join(filter_parts)}}}}}'
2026-03-10 10:04:03 +01:00
return cls._build("OverviewPage", variables)
2026-03-09 20:37:37 +01:00
2025-10-29 20:23:50 +01:00
class RTLPlusHeaders:
"""Standard header configurations for RTL+ requests"""
2026-04-23 22:36:36 +02:00
# Common headers shared across multiple request types
_COMMON_HEADERS = {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "de-DE,de;q=0.9",
"cache-control": "no-cache",
}
2025-10-29 20:23:50 +01:00
@staticmethod
def get_base_headers(user_agent: str = None) -> dict:
2026-04-23 20:55:52 +02:00
return {"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT, "Accept": "application/json"}
2025-10-29 20:23:50 +01:00
@staticmethod
def get_auth_headers(user_agent: str = None) -> dict:
headers = RTLPlusHeaders.get_base_headers(user_agent)
2026-04-23 20:55:52 +02:00
headers.update({
"Content-Type": "application/x-www-form-urlencoded",
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
})
2025-10-29 20:23:50 +01:00
return headers
@staticmethod
2026-04-23 22:36:36 +02:00
def get_api_headers(access_token: str = None, device_id: str = None, client_version: str = None,
user_agent: str = None) -> dict:
2025-10-29 20:23:50 +01:00
headers = RTLPlusHeaders.get_base_headers(user_agent)
2026-04-23 20:55:52 +02:00
headers.update({
"Content-Type": "application/json",
"Rtlplus-Client-Id": f"rci:rtlplus:{RTLPlusDefaults.PLATFORM_DEFAULT}",
"Rtlplus-Referrer": "",
"Rtlplus-Client-Version": client_version or _get_rtlplus_client_version(),
})
2025-10-29 20:23:50 +01:00
if access_token:
2026-01-06 16:41:03 +01:00
headers["Authorization"] = f"Bearer {access_token}"
2025-10-29 20:23:50 +01:00
if device_id:
2026-01-06 16:41:03 +01:00
headers["X-Device-Id"] = device_id
2025-10-29 20:23:50 +01:00
return headers
2026-03-07 15:19:07 +01:00
@staticmethod
2026-04-23 22:36:36 +02:00
def get_events_headers(access_token: str, profile_token: str = None, device_id: str = None,
client_version: str = None, user_agent: str = None) -> dict:
headers = RTLPlusHeaders.get_api_headers(access_token=access_token, device_id=device_id,
client_version=client_version, user_agent=user_agent)
2026-03-07 15:19:07 +01:00
if profile_token:
headers[RTLPlusDefaults.PROFILE_HEADER] = profile_token
2026-04-23 20:55:52 +02:00
return headers
2026-03-07 15:19:07 +01:00
2026-04-23 22:36:36 +02:00
@staticmethod
def get_bedrock_token_headers(
oauth_token: str,
device_id: str,
client_version: str,
user_agent: str,
auth_token: str,
timestamp: int,
2026-04-27 11:01:52 +02:00
profile_id: str = None,
2026-04-30 13:31:09 +02:00
user_id: str = None, # Add user_id parameter
2026-04-23 22:36:36 +02:00
) -> dict:
"""Headers for obtaining Bedrock token."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
"authorization": f"Bearer {oauth_token}",
2026-05-05 10:18:50 +02:00
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
2026-04-23 22:36:36 +02:00
"user-agent": user_agent,
2026-04-30 13:31:09 +02:00
"x-auth-device-id": device_id,
"x-auth-device-name": RTLPlusDefaults.DEVICE_NAME,
2026-04-23 22:36:36 +02:00
"x-auth-device-player-size-width": str(RTLPlusDefaults.DEVICE_PLAYER_SIZE_WIDTH),
"x-auth-device-player-size-height": str(RTLPlusDefaults.DEVICE_PLAYER_SIZE_HEIGHT),
"x-auth-token": auth_token,
"x-auth-token-timestamp": str(timestamp),
"x-client-release": client_version,
"x-customer-name": "rtlde",
})
2026-04-30 13:31:09 +02:00
# Add profile_id if available
2026-04-27 11:01:52 +02:00
if profile_id:
headers["x-auth-profile-id"] = profile_id
2026-04-30 13:31:09 +02:00
# Add user_id as x-auth-gigya-uid if available
if user_id:
headers["x-auth-gigya-uid"] = user_id
2026-04-23 22:36:36 +02:00
return headers
2026-04-23 20:55:52 +02:00
@staticmethod
def get_bedrock_layout_headers(
2026-04-23 22:36:36 +02:00
oauth_token: str,
bedrock_token: str,
device_id: str,
client_version: str,
user_agent: str,
location: str = None,
2026-04-23 20:55:52 +02:00
) -> dict:
2026-04-23 22:36:36 +02:00
"""Headers for Bedrock layout API calls."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
2026-04-23 20:55:52 +02:00
"authorization": f"Bearer {oauth_token}",
2026-05-05 10:18:50 +02:00
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
2026-04-29 23:51:44 +02:00
"request-timeout": "10000",
2026-04-23 20:55:52 +02:00
"user-agent": user_agent,
"x-bedrock-token": bedrock_token,
"x-client-release": client_version,
"x-customer-name": "rtlde",
2026-04-23 22:36:36 +02:00
})
2026-04-23 20:55:52 +02:00
if location:
headers["x-location"] = location
2026-03-07 15:19:07 +01:00
return headers
2026-04-23 20:55:52 +02:00
@staticmethod
2026-04-23 22:36:36 +02:00
def get_upfront_token_headers(
oauth_token: str,
bedrock_token: str,
device_id: str,
client_version: str,
user_agent: str,
) -> dict:
"""Headers for upfront token request."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
2026-04-23 20:55:52 +02:00
"authorization": f"Bearer {oauth_token}",
2026-05-05 10:18:50 +02:00
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
2026-04-23 20:55:52 +02:00
"user-agent": user_agent,
"x-bedrock-token": bedrock_token,
2026-04-30 13:31:09 +02:00
"x-auth-device-id": device_id,
"x-auth-device-name": RTLPlusDefaults.DEVICE_NAME,
2026-04-23 20:55:52 +02:00
"x-client-release": client_version,
"x-customer-name": "rtlde",
2026-04-23 22:36:36 +02:00
})
return headers
2026-04-23 20:55:52 +02:00
2025-10-29 20:23:50 +01:00
@staticmethod
2026-01-16 12:24:41 +01:00
def get_drm_headers(access_token: str, device_id: str = None, user_agent: str = None) -> dict:
2025-10-29 20:23:50 +01:00
return {
2026-03-08 19:40:16 +01:00
"Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"Referer": RTLPlusDefaults.BASE_WEBSITE,
"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT,
2026-01-06 16:41:03 +01:00
"X-Auth-Token": access_token,
"X-Device-Id": device_id or RTLPlusDefaults.DEVICE_ID,
"X-Device-Name": RTLPlusDefaults.DEVICE_NAME,
2025-10-29 20:23:50 +01:00
}
2026-04-27 11:01:52 +02:00
@staticmethod
def get_profiles_headers(
oauth_token: str,
bedrock_token: str,
client_version: str,
user_agent: str,
) -> dict:
"""Headers for fetching user profiles."""
headers = dict(RTLPlusHeaders._COMMON_HEADERS)
headers.update({
"authorization": f"Bearer {oauth_token}",
2026-05-05 10:18:50 +02:00
"origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"),
"referer": RTLPlusDefaults.BASE_WEBSITE,
2026-04-27 11:01:52 +02:00
"user-agent": user_agent,
"x-bedrock-token": bedrock_token,
"x-client-release": client_version,
"x-customer-name": "rtlde",
})
return headers
2025-10-29 20:23:50 +01:00
class RTLPlusConfig:
"""Configuration class that can be customized per instance"""
def __init__(self, config_dict: dict = None):
config = config_dict or {}
2026-01-06 16:41:03 +01:00
self.logo = config.get("logo", RTLPlusDefaults.RTLPLUS_LOGO)
2025-11-26 14:49:41 +01:00
2026-04-23 20:55:52 +02:00
# Core settings
self.client_version = config.get("client_version", _get_rtlplus_client_version())
2026-01-06 16:41:03 +01:00
self.device_id = config.get("device_id", RTLPlusDefaults.DEVICE_ID)
self.user_agent = config.get("user_agent", RTLPlusDefaults.USER_AGENT)
2026-03-09 11:41:57 +01:00
self.platform = config.get("platform", RTLPlusDefaults.PLATFORM_DEFAULT)
2025-10-29 20:23:50 +01:00
2026-04-23 22:36:36 +02:00
# Screen size
self.screen_width = config.get("screen_width", RTLPlusDefaults.DEVICE_PLAYER_SIZE_WIDTH)
self.screen_height = config.get("screen_height", RTLPlusDefaults.DEVICE_PLAYER_SIZE_HEIGHT)
2026-04-23 20:55:52 +02:00
# API endpoints
2026-01-06 16:41:03 +01:00
self.auth_endpoint = config.get("auth_endpoint", RTLPlusDefaults.AUTH_ENDPOINT)
2026-01-16 12:24:41 +01:00
self.graphql_endpoint = config.get("graphql_endpoint", RTLPlusDefaults.GRAPHQL_ENDPOINT)
2026-01-06 16:41:03 +01:00
self.base_website = config.get("base_website", RTLPlusDefaults.BASE_WEBSITE)
2026-01-16 12:24:41 +01:00
self.config_endpoint = config.get("config_endpoint", RTLPlusDefaults.CONFIG_ENDPOINT)
2025-10-29 20:23:50 +01:00
2026-04-23 20:55:52 +02:00
# Bedrock endpoints (Linear TV)
self.bedrock_auth_url = config.get("bedrock_auth_url", RTLPlusDefaults.BEDROCK_AUTH_URL)
self.bedrock_layout_base = config.get("bedrock_layout_base", RTLPlusDefaults.BEDROCK_LAYOUT_BASE)
self.bedrock_drm_upfront_base = config.get("bedrock_drm_upfront_base", RTLPlusDefaults.BEDROCK_DRM_UPFRONT_BASE)
self.bedrock_heartbeat_url = config.get("bedrock_heartbeat_url", RTLPlusDefaults.BEDROCK_HEARTBEAT_URL)
2026-04-23 22:36:36 +02:00
self.time_endpoint = config.get("time_endpoint", RTLPlusDefaults.TIME_ENDPOINT)
2026-04-27 11:01:52 +02:00
self.users_endpoint = config.get("users_endpoint", RTLPlusDefaults.USERS_ENDPOINT)
self.profiles_path = config.get("profiles_path", RTLPlusDefaults.PROFILES_PATH)
2026-04-23 20:55:52 +02:00
# 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)
self.preferred_drm_types = config.get("preferred_drm_types", RTLPlusDefaults.LINEAR_TV_PREFERRED_DRM_TYPES)
2025-10-29 20:23:50 +01:00
# HTTP settings
2026-01-06 16:41:03 +01:00
self.timeout = config.get("timeout", RTLPlusDefaults.DEFAULT_TIMEOUT)
2025-10-29 20:23:50 +01:00
2026-04-23 20:55:52 +02:00
def get_bedrock_layout_url(self, channel_seo: str) -> str:
"""Get Bedrock layout URL for a specific channel"""
return f"{self.bedrock_layout_base}/live/{channel_seo}/layout"
2026-04-27 11:01:52 +02:00
def get_profiles_url(self, user_id: str) -> str:
"""Get profiles URL for a specific user"""
return f"{self.users_endpoint}{self.profiles_path.format(user_id=user_id)}"
2026-04-23 20:55:52 +02:00
def get_epg_grid_url(self) -> str:
"""Get EPG grid URL for channel listing"""
return f"{self.bedrock_layout_base}/epg_grid"
def get_upfront_token_url(self, uid: str, content_id: str) -> str:
"""Get upfront token URL for DRM"""
return f"{self.bedrock_drm_upfront_base.format(uid=uid)}/{content_id}/upfront-token"
2025-10-29 20:23:50 +01:00
def get_base_headers(self) -> dict:
return RTLPlusHeaders.get_base_headers(self.user_agent)
def get_auth_headers(self) -> dict:
return RTLPlusHeaders.get_auth_headers(self.user_agent)
def get_api_headers(self, access_token: str = None) -> dict:
return RTLPlusHeaders.get_api_headers(
access_token=access_token,
device_id=self.device_id,
client_version=self.client_version,
2026-01-06 16:41:03 +01:00
user_agent=self.user_agent,
2025-10-29 20:23:50 +01:00
)
2026-03-07 15:19:07 +01:00
def get_events_headers(self, access_token: str, profile_token: str = None) -> dict:
return RTLPlusHeaders.get_events_headers(
access_token=access_token,
profile_token=profile_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
)
2026-04-27 11:01:52 +02:00
def get_profiles_headers(self, oauth_token: str, bedrock_token: str) -> dict:
"""Get headers for profiles request."""
return RTLPlusHeaders.get_profiles_headers(
oauth_token=oauth_token,
bedrock_token=bedrock_token,
client_version=self.client_version,
user_agent=self.user_agent,
)
def get_bedrock_token_headers(self, oauth_token: str, auth_token: str, timestamp: int,
2026-04-30 13:31:09 +02:00
profile_id: str = None, user_id: str = None) -> dict:
2026-04-23 22:36:36 +02:00
"""Get headers for Bedrock token request."""
return RTLPlusHeaders.get_bedrock_token_headers(
oauth_token=oauth_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
auth_token=auth_token,
timestamp=timestamp,
2026-04-27 11:01:52 +02:00
profile_id=profile_id,
2026-04-30 13:31:09 +02:00
user_id=user_id, # Pass through
2026-04-23 22:36:36 +02:00
)
2026-04-23 20:55:52 +02:00
def get_bedrock_layout_headers(self, oauth_token: str, bedrock_token: str, location: str = None) -> dict:
return RTLPlusHeaders.get_bedrock_layout_headers(
oauth_token=oauth_token,
bedrock_token=bedrock_token,
2025-10-29 20:23:50 +01:00
device_id=self.device_id,
2026-04-23 20:55:52 +02:00
client_version=self.client_version,
user_agent=self.user_agent,
location=location,
)
def get_upfront_token_headers(self, oauth_token: str, bedrock_token: str) -> dict:
return RTLPlusHeaders.get_upfront_token_headers(
oauth_token=oauth_token,
bedrock_token=bedrock_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
)
2026-04-28 18:54:26 +02:00
def get_layout_url(self, layout_type: str, content_id: str) -> str:
"""Get layout URL by type: 'live', 'video', 'folder', 'program', 'block'"""
path_map = {
"live": RTLPlusDefaults.LAYOUT_PATH_LIVE,
"video": RTLPlusDefaults.LAYOUT_PATH_VIDEO,
"folder": RTLPlusDefaults.LAYOUT_PATH_FOLDER,
"program": RTLPlusDefaults.LAYOUT_PATH_PROGRAM,
"block": RTLPlusDefaults.LAYOUT_PATH_BLOCK,
}
2026-04-29 10:59:05 +02:00
if layout_type not in path_map:
raise ValueError(f"Unknown layout type: {layout_type}. Valid types: {list(path_map.keys())}")
path = path_map[layout_type]
2026-05-01 13:41:02 +02:00
# Ensure content_id doesn't have type prefixes for the URL
clean_id = content_id
if ":" in clean_id:
# If it has a colon, extract the part after the colon
clean_id = clean_id.split(":", 1)[-1]
return f"{self.bedrock_layout_base}{path.format(id=clean_id)}"
2026-04-28 18:54:26 +02:00
def get_layout_headers(self, oauth_token: str, bedrock_token: str, location: str = None) -> dict:
"""Common headers for all layout requests"""
return RTLPlusHeaders.get_bedrock_layout_headers(
oauth_token=oauth_token,
bedrock_token=bedrock_token,
device_id=self.device_id,
client_version=self.client_version,
user_agent=self.user_agent,
location=location,
2026-05-15 17:24:39 +02:00
)
@property
def users_base_url(self) -> str:
"""Base URL for user-related API endpoints (bookmarks, etc.)"""
return "https://users.rtlde.bedrock.tech/v4/rtlde/m6group_web"