diff --git a/lib/streaming_providers/providers/rtlplus/constants.py b/lib/streaming_providers/providers/rtlplus/constants.py index 7bd84b2..5804b5c 100644 --- a/lib/streaming_providers/providers/rtlplus/constants.py +++ b/lib/streaming_providers/providers/rtlplus/constants.py @@ -152,7 +152,8 @@ class RTLPlusGraphQL: "WatchPlayerConfigV3":"fea0311fb572b6fded60c5a1a9d652f97f55d182bc4cedbdad676354a8d2797c", "SeoUrlData": "fcc4a812d6b93496f00c3068234db7722f553032bb760e09e5e6c74586c86f8d", "OverviewPage": "28aad4e992bb63330bfcd40a6906af3119d8a2612fa9fd28dae9c19127e247ca", - "LiveEventsOverview": "77a8f26d5de76daf801fcd7ae54bebd0ecabbeeb3ecb90889bc4a2e5590b7d20", + "LiveEventsOverviewPage": "77a8f26d5de76daf801fcd7ae54bebd0ecabbeeb3ecb90889bc4a2e5590b7d20", + "SeasonWithFormatAndEpisodes": "cc0fbbe17143f549a35efa6f8665ceb9b1cfae44b590f0b2381a9a304304c584", } @classmethod @@ -184,10 +185,18 @@ class RTLPlusGraphQL: f'{{"area":"{area}","offset":{offset},"take":{take}}}', ) + @classmethod + def season_with_episodes(cls, season_rrn: str, offset: int = 0, limit: int = 48) -> dict: + """SeasonWithFormatAndEpisodes — episodes for a specific season RRN.""" + return cls._build( + "SeasonWithFormatAndEpisodes", + f'{{"seasonId":"{season_rrn}","offset":{offset},"limit":{limit}}}', + ) + @classmethod def live_events_overview_page(cls) -> dict: """LiveEventsOverview — all upcoming/live events grouped by teaserRows.""" - return cls._build("LiveEventsOverview", "{}") + return cls._build("LiveEventsOverviewPage", "{}") # --- VOD --- diff --git a/lib/streaming_providers/providers/rtlplus/provider.py b/lib/streaming_providers/providers/rtlplus/provider.py index b0e49e0..b36411c 100644 --- a/lib/streaming_providers/providers/rtlplus/provider.py +++ b/lib/streaming_providers/providers/rtlplus/provider.py @@ -238,7 +238,7 @@ class RTLPlusProvider(StreamingProvider): teaser_rows = ( data.get("data", {}) - .get("liveEventsOverview", {}) + .get("liveEventsOverviewPage", {}) .get("teaserRows", []) ) diff --git a/lib/streaming_providers/providers/rtlplus/vod_manager.py b/lib/streaming_providers/providers/rtlplus/vod_manager.py index c7c1912..a4c0b64 100644 --- a/lib/streaming_providers/providers/rtlplus/vod_manager.py +++ b/lib/streaming_providers/providers/rtlplus/vod_manager.py @@ -143,9 +143,21 @@ class RTLPlusVodManager: if category_path[0].startswith(RTLPlusDefaults.VOD_SHOWS_ROOT_WATCH_PATH): return self._dispatch_shows(category_path) - # TopicWorlds branch — server RRNs only (rrn:multipurpose:...) - if depth == 1 and category_path[0].startswith("rrn:"): - return self._list_formats_for_topic(category_path[0]) + # RRN-namespace routing — dispatch by the kind of RRN, not depth. + # This handles format/season RRNs returned by OverviewPage for + # shows and serien (content_id = rrn:watch:videohub:format:* or season:*). + first = category_path[0] + if first.startswith("rrn:watch:videohub:format:"): + # Format RRN → list seasons + return self._list_seasons(first) + + if first.startswith("rrn:watch:videohub:season:"): + # Season RRN → list episodes via SeasonWithFormatAndEpisodes + return self._list_episodes_for_season(first) + + # TopicWorlds branch — rrn:multipurpose:* at depth 1 + if depth == 1 and first.startswith("rrn:"): + return self._list_formats_for_topic(first) if depth == 2: format_rrn = self._to_format_rrn(category_path[1]) @@ -541,6 +553,54 @@ class RTLPlusVodManager: ) return results + # ------------------------------------------------------------------ + # Season RRN → Episodes (SeasonWithFormatAndEpisodes) + # ------------------------------------------------------------------ + + def _list_episodes_for_season(self, season_rrn: str) -> List[VodItem]: + """ + Fetch episodes for a season identified directly by its RRN. + Uses SeasonWithFormatAndEpisodes (paginated). + Response shape: data.season.episodes[] + """ + results: List[VodItem] = [] + seen: set = set() + offset = 0 + limit = 48 + + while True: + data = self._graphql_get( + RTLPlusGraphQL.season_with_episodes(season_rrn, offset=offset, limit=limit) + ) + if not data: + break + + season = data.get("data", {}).get("season", {}) or {} + episodes = season.get("episodes", []) + + for ep in episodes: + if not ep: + continue + ep_id = ep.get("id", "") + if not ep_id or ep_id in seen: + continue + seen.add(ep_id) + vod = self._episode_to_item(ep) + if vod: + results.append(vod) + + # SeasonWithFormatAndEpisodes doesn't paginate via pageInfo; + # stop when we receive fewer items than requested. + if len(episodes) < limit: + break + offset += limit + + logger.debug( + f"RTLPlusVodManager: _list_episodes_for_season({season_rrn}) " + f"→ {len(results)} episodes" + ) + return results + # ------------------------------------------------------------------ # Depth 2 (series) — Seasons for a Format # ------------------------------------------------------------------ @@ -586,11 +646,11 @@ class RTLPlusVodManager: def _extract_seasons_from_format(data: dict) -> list: d = data.get("data", {}) fmt = d.get("format", {}) or d.get("Format", {}) or {} - return ( - fmt.get("seasons", {}).get("items", []) - or fmt.get("seasons", []) - or [] - ) + seasons = fmt.get("seasons", []) + # seasons may be a list directly or {"items": [...]} + if isinstance(seasons, dict): + seasons = seasons.get("items", []) + return seasons or [] # ------------------------------------------------------------------ # Depth 3 (series) — Episodes for a Season @@ -786,10 +846,17 @@ class RTLPlusVodManager: discriminator = server_id title = title or server_id + # Prefer the real season RRN when available (from Format.seasons[].id) + # so the client can pass it back and route via _list_episodes_for_season. + # Fall back to synthetic key for MRE nodes that lack an explicit id. + season_rrn = node.get("id", "") + content_id = season_rrn if season_rrn.startswith("rrn:watch:videohub:season:") else f"season:{discriminator}@{format_rrn}" + return VodCategory( name=title, - content_id=f"season:{discriminator}@{format_rrn}", + content_id=content_id, provider="rtlplus", + child_count=node.get("numberOfEpisodes"), ) @staticmethod