# streaming_providers/providers/rtlplus/constants.py """ RTL+ provider constants and default configurations """ from ..globals import get_user_agent import json import urllib.request from typing import Optional from ...base.utils.logger import logger class RTLPlusDefaults: """Default values for RTL+ provider""" RTLPLUS_LOGO = "https://upload.wikimedia.org/wikipedia/commons/0/0f/RTL_DE_Logo_2021_pink_blau_gruen.png" # Device information # DEVICE_ID = "8c3f37cc-13a3-4141-bd0f-e4b3673fe5e4" DEVICE_ID = "_luid_f4f52150-0645-43f8-a8f5-029341f49980" DEVICE_NAME = "Linux Chrome" PLAYREADY_DEVICE_NAME = "Windows Edge" # Device screen size (used in headers) DEVICE_PLAYER_SIZE_WIDTH = 3840 DEVICE_PLAYER_SIZE_HEIGHT = 2160 # User agents USER_AGENT = get_user_agent("windows", "chrome") PLAYREADY_USER_AGENT = get_user_agent("windows", "edge") # Supported platform identifiers PLATFORM_WEB = "web" PLATFORM_DEFAULT = PLATFORM_WEB # Client and version information CLIENT_VERSION_FALLBACK = "2025.6.26.0" CLIENT_ID = f"rtlplus-{PLATFORM_DEFAULT}" BEDROCK_CLIENT_ID = "bedrock-m6group_web" # 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" # API endpoints AUTH_REALM_BASE = "https://auth.rtl.de/auth/realms/rtlplus" AUTH_BASE_URL = f"{AUTH_REALM_BASE}/protocol/openid-connect" AUTH_ENDPOINT = f"{AUTH_BASE_URL}/token" # 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" 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" # 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" TIME_ENDPOINT = "https://time.rtlde.bedrock.tech/" USERS_ENDPOINT = "https://users.rtlde.bedrock.tech" PROFILES_PATH = "/v2/platforms/m6group_web/users/{user_id}/profiles" VERSION_ENDPOINT = "https://beta.plus.rtl.de/version.json" # DRM Request Headers (common for all DRM types) DRM_COMMON_HEADERS = { "origin": BASE_WEBSITE.rstrip("/"), "referer": BASE_WEBSITE, } # Direct CDN ORIGIN_CDN_BASE = "https://origin.live.rtlde.bedrock.tech" # 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" IMAGE_PARAMS_LOGO = "auto=avif&blur=0&fit=scale_crop&height=1080&interlace=1&optimize=high&width=1080" IMAGE_SIGNING_KEY = "x9vGg4RNeNBqV2nBfhqLV6cN4n" # Anonymous credentials (fallback) ANONYMOUS_CLIENT_ID = "anonymous-user" ANONYMOUS_CLIENT_SECRET = "4bfeb73f-1c4a-4e9f-a7fa-96aa1ad3d94c" # HTTP settings DEFAULT_TIMEOUT = 30 # Custom header carrying the signed profile JWT 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" # Linear TV Quality Presets LINEAR_TV_PREFERRED_QUALITIES = ["hd", "sd"] LINEAR_TV_PREFERRED_FORMATS = ["dashcenc", "hlsfp"] # 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. LINEAR_TV_PREFERRED_DRM_TYPES = ["hardware", "software"] # 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]) # 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 # --------------------------------------------------------------------------- # VOD — stream config endpoints # --------------------------------------------------------------------------- VOD_PREFERRED_VARIANTS = ["dashhd", "dashsd", "hlsfairplayhd", "hlsfairplaysd"] VOD_PLATFORM_GRAPHQL = PLATFORM_DEFAULT.upper() VOD_WURSTLAND_CONFIG_URL = "https://wurstland.plus.rtl.de/config/{rrn}/{platform}" VOD_WURSTLAND_PLATFORM = PLATFORM_DEFAULT.upper() # --------------------------------------------------------------------------- # VOD — root category definition # --------------------------------------------------------------------------- VOD_ROOT_SLUG = "topic-worlds" VOD_MOVIES_ROOT_WATCH_PATH = "/video-tv/filme" VOD_MOVIES_GENRE_WATCH_PATH = "/video-tv/filme/genre" VOD_MOVIES_ROOT_ID = "/video-tv/filme" VOD_MOVIE_GENRE_SLUGS = [ "action", "abenteuer", "animation", "comedy", "dokumentation", "drama", "fantasy", "horror", "kinder", "krimi", "liebesfilm", "science-fiction", "thriller", ] VOD_SERIES_ROOT_WATCH_PATH = "/video-tv/serien" VOD_SERIES_GENRE_WATCH_PATH = "/video-tv/serien/genre" VOD_SERIES_ROOT_ID = "/video-tv/serien" VOD_SERIES_GENRE_SLUGS = [ "action", "animation", "comedy", "crime", "dokumentation", "drama", "fantasy", "horror", "kinder", "reality", "romance", "science-fiction", "thriller", ] VOD_SHOWS_ROOT_WATCH_PATH = "/video-tv/shows" VOD_SHOWS_GENRE_WATCH_PATH = "/video-tv/shows/genre" VOD_SHOWS_ROOT_ID = "/video-tv/shows" VOD_SHOW_GENRE_SLUGS = [ "action", "comedy", "crime", "dokumentation", "drama", "kinder", "reality", "romance", "thriller", ] VOD_ELEMENT_TYPE_MOVIE = "MOVIE" VOD_ELEMENT_TYPE_SERIES = "SERIES" VOD_ELEMENT_TYPE_SHOW = "SHOW" VOD_OVERVIEW_PAGE_LIMIT = 48 # Lazy-loaded client version _CLIENT_VERSION_CACHE: Optional[str] = None def _get_rtlplus_client_version() -> str: """Lazy fetch RTL+ client version from version.json endpoint.""" global _CLIENT_VERSION_CACHE if _CLIENT_VERSION_CACHE is None: try: with urllib.request.urlopen(RTLPlusDefaults.VERSION_ENDPOINT, timeout=4) as resp: data = json.loads(resp.read().decode()) _CLIENT_VERSION_CACHE = data["version"] except Exception as exc: logger.warning( f"Could not fetch RTL+ client version ({exc}); using fallback {RTLPlusDefaults.CLIENT_VERSION_FALLBACK}") _CLIENT_VERSION_CACHE = RTLPlusDefaults.CLIENT_VERSION_FALLBACK return _CLIENT_VERSION_CACHE class RTLPlusGraphQL: """Persisted-query hashes and parameter builders for all RTL+ GraphQL operations.""" HASHES: dict[str, str] = { "LiveTvStations": "845cf56a2a78110a0f978c1a2af2bc7f9a1c937d0f324ffaf852a9a4414c8485", "ExploreWidgetWatch": "724f21ab86aa3f8c57673a3b346cf119f6a155bf82bb0201fd3b18e28e44f1ed", "TopicWorlds": "3dbde4c45532f4bdb0a1d7c210db43f0888f8a82f4aab43f7a90f3c4762d8ff7", "Format": "d112638c0184ab5698af7b69532dfe2f12973f7af9cb137b9f70278130b1eafa", "MRE": "0c77404637570adff548e329a48654498c54ce1c36a459d72586ff18999bebaa", "Episode": "87dbde15a0d269b11606f5ff458d555e98eb493bb4fb6ddc150d812d5e9a9cf8", "WatchPlayerConfigV3": "fea0311fb572b6fded60c5a1a9d652f97f55d182bc4cedbdad676354a8d2797c", "SeoUrlData": "fcc4a812d6b93496f00c3068234db7722f553032bb760e09e5e6c74586c86f8d", "OverviewPage": "28aad4e992bb63330bfcd40a6906af3119d8a2612fa9fd28dae9c19127e247ca", "LiveEventsOverviewPage": "77a8f26d5de76daf801fcd7ae54bebd0ecabbeeb3ecb90889bc4a2e5590b7d20", "SeasonWithFormatAndEpisodes": "cc0fbbe17143f549a35efa6f8665ceb9b1cfae44b590f0b2381a9a304304c584", } @classmethod def _build(cls, operation: str, variables: str) -> dict: return { "operationName": operation, "variables": variables, "extensions": f'{{"persistedQuery":{{"version":1,"sha256Hash":"{cls.HASHES[operation]}"}}}}', } @classmethod def live_tv_stations(cls, epg_count: int = 4) -> dict: return cls._build("LiveTvStations", f'{{"epgCount":{epg_count},"filter":{{"channelTypes":["BROADCAST","FAST"]}}}}') @classmethod def explore_widget_watch(cls, area: str = "home", offset: int = 0, take: int = 15) -> dict: return cls._build("ExploreWidgetWatch", f'{{"area":"{area}","offset":{offset},"take":{take}}}') @classmethod def season_with_episodes(cls, season_rrn: str, offset: int = 0, limit: int = 48) -> dict: return cls._build("SeasonWithFormatAndEpisodes", f'{{"seasonId":"{season_rrn}","offset":{offset},"limit":{limit}}}') @classmethod def live_events_overview_page(cls) -> dict: return cls._build("LiveEventsOverviewPage", "{}") @classmethod def topic_worlds(cls, take: int = 100, offset: int = 0) -> dict: return cls._build("TopicWorlds", f'{{"take":{take},"offset":{offset},"filterForSearchGrid":true}}') @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: return cls._build("WatchPlayerConfigV3", f'{{"platform":"{RTLPlusDefaults.VOD_PLATFORM_GRAPHQL}","id":"{rrn}"}}') @classmethod def seo_url_data(cls, watch_path: str) -> dict: return cls._build("SeoUrlData", f'{{"watchPath":"{watch_path}"}}') @classmethod def overview_page(cls, element_type: str, genres: list = None, offset: int = 0, limit: int = None) -> dict: 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}') variables = f'{{"pagination":{{"offset":{offset},"limit":{limit}}},"filter":{{{",".join(filter_parts)}}}}}' return cls._build("OverviewPage", variables) class RTLPlusHeaders: """Standard header configurations for RTL+ requests""" # 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", } @staticmethod def get_base_headers(user_agent: str = None) -> dict: return {"User-Agent": user_agent or RTLPlusDefaults.USER_AGENT, "Accept": "application/json"} @staticmethod def get_auth_headers(user_agent: str = None) -> dict: headers = RTLPlusHeaders.get_base_headers(user_agent) headers.update({ "Content-Type": "application/x-www-form-urlencoded", "Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), "Referer": RTLPlusDefaults.BASE_WEBSITE, }) return headers @staticmethod def get_api_headers(access_token: str = None, device_id: str = None, client_version: str = None, user_agent: str = None) -> dict: headers = RTLPlusHeaders.get_base_headers(user_agent) 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(), }) if access_token: headers["Authorization"] = f"Bearer {access_token}" if device_id: headers["X-Device-Id"] = device_id return headers @staticmethod 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) if profile_token: headers[RTLPlusDefaults.PROFILE_HEADER] = profile_token return headers @staticmethod def get_bedrock_token_headers( oauth_token: str, device_id: str, client_version: str, user_agent: str, auth_token: str, timestamp: int, profile_id: str = None, user_id: str = None, # Add user_id parameter ) -> dict: """Headers for obtaining Bedrock token.""" headers = dict(RTLPlusHeaders._COMMON_HEADERS) headers.update({ "authorization": f"Bearer {oauth_token}", "origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), "referer": RTLPlusDefaults.BASE_WEBSITE, "user-agent": user_agent, "x-auth-device-id": device_id, "x-auth-device-name": RTLPlusDefaults.DEVICE_NAME, "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", }) # Add profile_id if available if profile_id: headers["x-auth-profile-id"] = profile_id # Add user_id as x-auth-gigya-uid if available if user_id: headers["x-auth-gigya-uid"] = user_id return headers @staticmethod def get_bedrock_layout_headers( oauth_token: str, bedrock_token: str, device_id: str, client_version: str, user_agent: str, location: str = None, ) -> dict: """Headers for Bedrock layout API calls.""" headers = dict(RTLPlusHeaders._COMMON_HEADERS) headers.update({ "authorization": f"Bearer {oauth_token}", "origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), "referer": RTLPlusDefaults.BASE_WEBSITE, "request-timeout": "10000", "user-agent": user_agent, "x-bedrock-token": bedrock_token, "x-client-release": client_version, "x-customer-name": "rtlde", }) if location: headers["x-location"] = location return headers @staticmethod 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({ "authorization": f"Bearer {oauth_token}", "origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), "referer": RTLPlusDefaults.BASE_WEBSITE, "user-agent": user_agent, "x-bedrock-token": bedrock_token, "x-auth-device-id": device_id, "x-auth-device-name": RTLPlusDefaults.DEVICE_NAME, "x-client-release": client_version, "x-customer-name": "rtlde", }) return headers @staticmethod def get_drm_headers(access_token: str, device_id: str = None, user_agent: str = None) -> dict: return { "Origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), "Referer": RTLPlusDefaults.BASE_WEBSITE, "User-Agent": user_agent or RTLPlusDefaults.USER_AGENT, "X-Auth-Token": access_token, "X-Device-Id": device_id or RTLPlusDefaults.DEVICE_ID, "X-Device-Name": RTLPlusDefaults.DEVICE_NAME, } @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}", "origin": RTLPlusDefaults.BASE_WEBSITE.rstrip("/"), "referer": RTLPlusDefaults.BASE_WEBSITE, "user-agent": user_agent, "x-bedrock-token": bedrock_token, "x-client-release": client_version, "x-customer-name": "rtlde", }) return headers class RTLPlusConfig: """Configuration class that can be customized per instance""" def __init__(self, config_dict: dict = None): config = config_dict or {} self.logo = config.get("logo", RTLPlusDefaults.RTLPLUS_LOGO) # Core settings self.client_version = config.get("client_version", _get_rtlplus_client_version()) self.device_id = config.get("device_id", RTLPlusDefaults.DEVICE_ID) self.user_agent = config.get("user_agent", RTLPlusDefaults.USER_AGENT) self.platform = config.get("platform", RTLPlusDefaults.PLATFORM_DEFAULT) # 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) # API endpoints self.auth_endpoint = config.get("auth_endpoint", RTLPlusDefaults.AUTH_ENDPOINT) self.graphql_endpoint = config.get("graphql_endpoint", RTLPlusDefaults.GRAPHQL_ENDPOINT) self.base_website = config.get("base_website", RTLPlusDefaults.BASE_WEBSITE) self.config_endpoint = config.get("config_endpoint", RTLPlusDefaults.CONFIG_ENDPOINT) # 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) self.time_endpoint = config.get("time_endpoint", RTLPlusDefaults.TIME_ENDPOINT) self.users_endpoint = config.get("users_endpoint", RTLPlusDefaults.USERS_ENDPOINT) self.profiles_path = config.get("profiles_path", RTLPlusDefaults.PROFILES_PATH) # 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) # HTTP settings self.timeout = config.get("timeout", RTLPlusDefaults.DEFAULT_TIMEOUT) 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" 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)}" 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" 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, user_agent=self.user_agent, ) 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, ) 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, profile_id: str = None, user_id: str = None) -> dict: """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, profile_id=profile_id, user_id=user_id, # Pass through ) 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, device_id=self.device_id, 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, ) 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, } 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] # 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)}" 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, ) @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"