Add discovery

This commit is contained in:
Nirvana
2026-03-10 13:04:11 +01:00
parent f0e13c992d
commit 4c333c458b
3 changed files with 88 additions and 12 deletions
@@ -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 ---
@@ -238,7 +238,7 @@ class RTLPlusProvider(StreamingProvider):
teaser_rows = (
data.get("data", {})
.get("liveEventsOverview", {})
.get("liveEventsOverviewPage", {})
.get("teaserRows", [])
)
@@ -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