diff --git a/lib/streaming_providers/providers/rtlplus/constants.py b/lib/streaming_providers/providers/rtlplus/constants.py index 054ac2b..31bedaa 100644 --- a/lib/streaming_providers/providers/rtlplus/constants.py +++ b/lib/streaming_providers/providers/rtlplus/constants.py @@ -103,9 +103,8 @@ class RTLPlusDefaults: # --------------------------------------------------------------------------- # VOD — root category definition + # TopicWorlds is the real browse API — slug "topic-worlds" maps to the root # --------------------------------------------------------------------------- - - # TopicWorlds is the series/show browse API — slug "topic-worlds" maps to the root VOD_ROOT_SLUG = "topic-worlds" # --------------------------------------------------------------------------- @@ -124,77 +123,9 @@ class RTLPlusDefaults: # Episode — full episode detail by RRN; variables: { rrn } VOD_HASH_EPISODE = "87dbde15a0d269b11606f5ff458d555e98eb493bb4fb6ddc150d812d5e9a9cf8" - # WatchPlayerConfigV3 — stream URL + DRM config; variables: { platform, id } + # WatchPlayerConfigV3 — stream + DRM config; variables: { platform, id } VOD_HASH_WATCH_PLAYER = "fea0311fb572b6fded60c5a1a9d652f97f55d182bc4cedbdad676354a8d2797c" - # SeoUrlData — resolve watch-path hierarchy (breadcrumbs + RRI page-type) - # Used to enumerate genre slugs under /video-tv/filme/genre/* and - # /video-tv/serien/genre/* - # variables: { watchPath } - VOD_HASH_SEO_URL_DATA = "fcc4a812d6b93496f00c3068234db7722f553032bb760e09e5e6c74586c86f8d" - - # OverviewPage — paginated grid for both movies AND series, filtered by - # elementType ("MOVIE" | "SERIES") and optional genres list. - # variables: { pagination: { offset, limit }, filter: { elementType, genres? } } - VOD_HASH_OVERVIEW_PAGE = "28aad4e992bb63330bfcd40a6906af3119d8a2612fa9fd28dae9c19127e247ca" - - # OverviewPage elementType values - VOD_ELEMENT_TYPE_MOVIE = "MOVIE" - VOD_ELEMENT_TYPE_SERIES = "SERIES" - - # Default page size for OverviewPage pagination - VOD_OVERVIEW_PAGE_LIMIT = 48 - - # --------------------------------------------------------------------------- - # VOD — Movies root / genre constants - # --------------------------------------------------------------------------- - VOD_MOVIES_ROOT_WATCH_PATH = "/video-tv/filme" - VOD_MOVIES_GENRE_WATCH_PATH = "/video-tv/filme/genre" - VOD_MOVIES_ROOT_ID = "movies-genre:/video-tv/filme" - VOD_MOVIES_GENRE_PREFIX = "movies-genre:" - - # Known movie genre slugs (seed list; validated at runtime via SeoUrlData) - VOD_MOVIE_GENRE_SLUGS = [ - "action", - "abenteuer", - "animation", - "comedy", - "dokumentation", - "drama", - "fantasy", - "horror", - "kinder", - "krimi", - "liebesfilm", - "science-fiction", - "thriller", - ] - - # --------------------------------------------------------------------------- - # VOD — Series root / genre constants - # --------------------------------------------------------------------------- - VOD_SERIES_ROOT_WATCH_PATH = "/video-tv/serien" - VOD_SERIES_GENRE_WATCH_PATH = "/video-tv/serien/genre" - VOD_SERIES_ROOT_ID = "series-genre:/video-tv/serien" - VOD_SERIES_GENRE_PREFIX = "series-genre:" - - # Known series genre slugs (seed list; validated at runtime via SeoUrlData) - VOD_SERIES_GENRE_SLUGS = [ - "action", - "animation", - "comedy", - "crime", - "dokumentation", - "drama", - "fantasy", - "horror", - "kinder", - "reality", - "romance", - "science-fiction", - "thriller", - ] - class RTLPlusHeaders: """Standard header configurations for RTL+ requests""" @@ -205,6 +136,11 @@ class RTLPlusHeaders: return { "User-Agent": user_agent or RTLPlusDefaults.USER_AGENT, "Accept": "application/json", + # 'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8', + # 'Accept-Encoding': 'gzip, deflate, br', + # 'DNT': '1', + # 'Connection': 'keep-alive', + # 'Upgrade-Insecure-Requests': '1' } @staticmethod @@ -254,6 +190,25 @@ class RTLPlusHeaders: client_version: str = None, user_agent: str = None, ) -> dict: + """ + Get headers for the editorial GraphQL endpoint (events / ExploreWidgetWatch). + + Extends the standard API headers with the optional ``Rtlplus-Profile`` + JWT that the browser sends when a user profile is active. The header + is omitted when *profile_token* is ``None`` so that anonymous / client- + credential sessions work without modification. + + Args: + access_token: Bearer token from the auth endpoint. + profile_token: Signed profile JWT (``Rtlplus-Profile`` header). + Pass ``None`` for anonymous / client-credential sessions. + device_id: Device UUID forwarded as ``X-Device-Id``. + client_version: Client version string; falls back to the default. + user_agent: UA string; falls back to the default. + + Returns: + dict of HTTP headers ready for use with the GraphQL endpoint. + """ headers = RTLPlusHeaders.get_api_headers( access_token=access_token, device_id=device_id, @@ -283,6 +238,8 @@ class RTLPlusHeaders: @staticmethod def get_event_manifest_headers(access_token: str, user_agent: str = None) -> dict: + """Get headers for the event manifest endpoint (stus.player.streamingtech.de/liveevent). + Uses X-Auth-Token scheme as required by this endpoint.""" return { "X-Auth-Token": access_token, "Content-Type": "application/json", @@ -335,20 +292,29 @@ class RTLPlusConfig: self.timeout = config.get("timeout", RTLPlusDefaults.DEFAULT_TIMEOUT) def get_manifest_url(self, content_id: str) -> str: - if "live-events" in content_id: # rrn:watch:live-events:*:{id} - return self.event_manifest_endpoint.format(content_id=content_id, platform=self.platform) + if "live-events" in content_id: + return self.event_manifest_endpoint.format( + content_id=content_id, platform=self.platform + ) if content_id.startswith("rrn:watch:videohub:station:"): - return self.manifest_endpoint.format(channel_id=content_id, platform=self.platform) + return self.manifest_endpoint.format( + channel_id=content_id, platform=self.platform + ) # episode, movie, clip — full RRN passed directly - return self.vod_playout_endpoint.format(content_id=content_id, platform=self.platform) + return self.vod_playout_endpoint.format( + content_id=content_id, platform=self.platform + ) def get_base_headers(self) -> dict: + """Get base headers with this config's user agent""" return RTLPlusHeaders.get_base_headers(self.user_agent) def get_auth_headers(self) -> dict: + """Get auth headers with this config's settings""" return RTLPlusHeaders.get_auth_headers(self.user_agent) def get_api_headers(self, access_token: str = None) -> dict: + """Get API headers with this config's settings""" return RTLPlusHeaders.get_api_headers( access_token=access_token, device_id=self.device_id, @@ -357,6 +323,16 @@ class RTLPlusConfig: ) def get_events_headers(self, access_token: str, profile_token: str = None) -> dict: + """ + Get headers for the editorial GraphQL endpoint with this config's settings. + + Args: + access_token: Bearer token from the auth endpoint. + profile_token: Optional signed profile JWT (``Rtlplus-Profile``). + + Returns: + dict of HTTP headers. + """ return RTLPlusHeaders.get_events_headers( access_token=access_token, profile_token=profile_token, @@ -366,6 +342,7 @@ class RTLPlusConfig: ) def get_drm_headers(self, access_token: str) -> dict: + """Get DRM headers with this config's settings""" return RTLPlusHeaders.get_drm_headers( access_token=access_token, device_id=self.device_id, @@ -373,12 +350,14 @@ class RTLPlusConfig: ) def get_event_manifest_headers(self, access_token: str) -> dict: + """Get event manifest headers with this config's settings.""" return RTLPlusHeaders.get_event_manifest_headers( access_token=access_token, user_agent=self.user_agent, ) def get_playready_drm_headers(self, access_token: str) -> dict: + """Get PlayReady-specific DRM headers (Edge UA, SOAP content-type)""" return RTLPlusHeaders.get_playready_drm_headers( access_token=access_token, device_id=self.device_id, @@ -397,45 +376,57 @@ class RTLPlusConfig: @staticmethod def get_vod_topic_worlds_params(take: int = 100, offset: int = 0) -> dict: + """ + TopicWorlds — browse all genre/topic worlds. + + Confirmed from live traffic: + operationName: TopicWorlds + variables: { take, offset, filterForSearchGrid: true } + response key: data.topicWorldsV2.elements[] (NOT topicWorlds.items[]) + pagination: data.topicWorldsV2.pageInfo.hasNextPage + """ return { "operationName": "TopicWorlds", "variables": f'{{"take":{take},"offset":{offset},"filterForSearchGrid":true}}', "extensions": ( '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_TOPIC_WORLDS}"}}' + f'"{RTLPlusDefaults.VOD_HASH_TOPIC_WORLDS}"}}}}' ), } @staticmethod def get_vod_format_params(format_rrn: str) -> dict: + """Format — full format detail (seasons, metadata) by RRN.""" return { "operationName": "Format", "variables": f'{{"id":"{format_rrn}"}}', "extensions": ( '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_FORMAT}"}}' + f'"{RTLPlusDefaults.VOD_HASH_FORMAT}"}}}}' ), } @staticmethod def get_vod_mre_params(format_rrn: str) -> dict: + """MRE — seasons + related episodes for a Format.""" return { "operationName": "MRE", "variables": f'{{"id":"{format_rrn}"}}', "extensions": ( '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_MRE}"}}' + f'"{RTLPlusDefaults.VOD_HASH_MRE}"}}}}' ), } @staticmethod def get_vod_episode_params(episode_rrn: str) -> dict: + """Episode — full episode detail by RRN.""" return { "operationName": "Episode", "variables": f'{{"rrn":"{episode_rrn}"}}', "extensions": ( '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_EPISODE}"}}' + f'"{RTLPlusDefaults.VOD_HASH_EPISODE}"}}}}' ), } @@ -450,73 +441,6 @@ class RTLPlusConfig: ), "extensions": ( '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_WATCH_PLAYER}"}}' - ), - } - - @staticmethod - def get_vod_seo_url_data_params(watch_path: str) -> dict: - """ - SeoUrlData — resolve a watch-path to its hierarchy + RRI page-type. - - Works for any watch-path: - /video-tv/filme - /video-tv/filme/genre/action - /video-tv/serien - /video-tv/serien/genre/drama - - variables: { watchPath } - """ - return { - "operationName": "SeoUrlData", - "variables": f'{{"watchPath":"{watch_path}"}}', - "extensions": ( - '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_SEO_URL_DATA}"}}' - ), - } - - @staticmethod - def get_vod_overview_page_params( - element_type: str, - genres: list = None, - offset: int = 0, - limit: int = None, - ) -> dict: - """ - OverviewPage — paginated grid for movies or series with optional genre filter. - - This single query covers both the MovieGrid and SeriesGrid use-cases: - element_type = RTLPlusDefaults.VOD_ELEMENT_TYPE_MOVIE → movies - element_type = RTLPlusDefaults.VOD_ELEMENT_TYPE_SERIES → series - - Args: - element_type: "MOVIE" or "SERIES" - genres: Optional list of genre slug strings, e.g. ["drama"]. - Pass None / [] to fetch all genres. - offset: Pagination offset (0-based). - limit: Page size (defaults to VOD_OVERVIEW_PAGE_LIMIT). - - variables: { pagination: { offset, limit }, filter: { elementType, genres? } } - """ - 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}}},' - f'"filter":{{{",".join(filter_parts)}}}}}' - ) - - return { - "operationName": "OverviewPage", - "variables": variables, - "extensions": ( - '{"persistedQuery":{"version":1,"sha256Hash":' - f'"{RTLPlusDefaults.VOD_HASH_OVERVIEW_PAGE}"}}' + f'"{RTLPlusDefaults.VOD_HASH_WATCH_PLAYER}"}}}}' ), } \ No newline at end of file