mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-21 16:42:18 +02:00
Add discovery
This commit is contained in:
@@ -114,9 +114,20 @@ class RTLPlusDefaults:
|
||||
"romance", "science-fiction", "thriller",
|
||||
]
|
||||
|
||||
# Shows root / genre constants
|
||||
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",
|
||||
]
|
||||
|
||||
# OverviewPage elementType values and pagination
|
||||
VOD_ELEMENT_TYPE_MOVIE = "MOVIE"
|
||||
VOD_ELEMENT_TYPE_SERIES = "SERIES"
|
||||
VOD_ELEMENT_TYPE_SHOW = "SHOW"
|
||||
VOD_OVERVIEW_PAGE_LIMIT = 48
|
||||
|
||||
|
||||
@@ -139,8 +150,9 @@ class RTLPlusGraphQL:
|
||||
"MRE": "0c77404637570adff548e329a48654498c54ce1c36a459d72586ff18999bebaa",
|
||||
"Episode": "87dbde15a0d269b11606f5ff458d555e98eb493bb4fb6ddc150d812d5e9a9cf8",
|
||||
"WatchPlayerConfigV3":"fea0311fb572b6fded60c5a1a9d652f97f55d182bc4cedbdad676354a8d2797c",
|
||||
"SeoUrlData": "fcc4a812d6b93496f00c3068234db7722f553032bb760e09e5e6c74586c86f8d",
|
||||
"OverviewPage": "28aad4e992bb63330bfcd40a6906af3119d8a2612fa9fd28dae9c19127e247ca",
|
||||
"SeoUrlData": "fcc4a812d6b93496f00c3068234db7722f553032bb760e09e5e6c74586c86f8d",
|
||||
"OverviewPage": "28aad4e992bb63330bfcd40a6906af3119d8a2612fa9fd28dae9c19127e247ca",
|
||||
"LiveEventsOverview": "77a8f26d5de76daf801fcd7ae54bebd0ecabbeeb3ecb90889bc4a2e5590b7d20",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -172,6 +184,11 @@ class RTLPlusGraphQL:
|
||||
f'{{"area":"{area}","offset":{offset},"take":{take}}}',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def live_events_overview_page(cls) -> dict:
|
||||
"""LiveEventsOverview — all upcoming/live events grouped by teaserRows."""
|
||||
return cls._build("LiveEventsOverview", "{}")
|
||||
|
||||
# --- VOD ---
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -200,21 +200,22 @@ class RTLPlusLiveEvent:
|
||||
|
||||
@classmethod
|
||||
def from_api_node(cls, node: Dict[str, Any]) -> "RTLPlusLiveEvent":
|
||||
"""Parse a single LiveEvent GraphQL node."""
|
||||
"""Parse a single LiveEvent GraphQL node from LiveEventsOverview."""
|
||||
details = node.get("details") or {}
|
||||
images = node.get("images") or {}
|
||||
return cls(
|
||||
id=node["id"],
|
||||
title=node["title"],
|
||||
stream_start=node["streamStart"],
|
||||
stream_end=node["streamEnd"],
|
||||
location=node.get("location"),
|
||||
location=details.get("stadium"),
|
||||
event_category=node.get("eventCategory", "GENERIC"),
|
||||
event_sub_category=node.get("eventSubCategory", ""),
|
||||
description=node.get("description"),
|
||||
short_description=node.get("shortDescription"),
|
||||
short_description=None,
|
||||
logo_url=(
|
||||
node.get("liveImages", {})
|
||||
.get("artworkLandscape", {})
|
||||
.get("url")
|
||||
images.get("artworkLandscape", {}).get("url")
|
||||
or images.get("artworkPortrait", {}).get("url")
|
||||
),
|
||||
required_permission=node.get(
|
||||
"requiredPermission", RTLPlusDefaults.PERMISSION_PAY_TV
|
||||
@@ -287,4 +288,4 @@ class RTLPlusStreamInfo:
|
||||
|
||||
def has_drm(self) -> bool:
|
||||
"""Check if stream has DRM protection"""
|
||||
return bool(self.drm_license_url and self.drm_key_id)
|
||||
return bool(self.drm_license_url and self.drm_key_id)
|
||||
@@ -209,13 +209,12 @@ class RTLPlusProvider(StreamingProvider):
|
||||
return events
|
||||
|
||||
def _fetch_live_events(self) -> List[RTLPlusLiveEvent]:
|
||||
access_token = self.authenticator.get_bearer_token()
|
||||
headers = self.rtl_config.get_events_headers(access_token=access_token)
|
||||
headers = self._get_rtlplus_authenticated_headers()
|
||||
|
||||
try:
|
||||
response = self.http_manager.get(
|
||||
self.rtl_config.graphql_endpoint,
|
||||
params=RTLPlusGraphQL.explore_widget_watch(),
|
||||
params=RTLPlusGraphQL.live_events_overview_page(),
|
||||
headers=headers,
|
||||
operation="api",
|
||||
)
|
||||
@@ -229,25 +228,30 @@ class RTLPlusProvider(StreamingProvider):
|
||||
@staticmethod
|
||||
def _parse_live_events(data: Dict[str, Any]) -> List[RTLPlusLiveEvent]:
|
||||
"""
|
||||
Extract LiveEvent nodes from the GraphQL response.
|
||||
Extract LiveEvent nodes from the LiveEventsOverview GraphQL response.
|
||||
|
||||
Response shape: data.liveEventsOverview.teaserRows[].events[]
|
||||
Static so it's unit-testable without provider state.
|
||||
"""
|
||||
events: List[RTLPlusLiveEvent] = []
|
||||
widgets = (
|
||||
seen: set = set()
|
||||
|
||||
teaser_rows = (
|
||||
data.get("data", {})
|
||||
.get("editorialView", {})
|
||||
.get("widgets", {})
|
||||
.get("items", [])
|
||||
.get("liveEventsOverview", {})
|
||||
.get("teaserRows", [])
|
||||
)
|
||||
|
||||
for widget in widgets:
|
||||
if widget.get("__typename") != "LiveEventWidget":
|
||||
continue
|
||||
for element in widget.get("elements", []):
|
||||
for row in teaser_rows:
|
||||
for element in (row.get("events") or []):
|
||||
if element is None:
|
||||
continue
|
||||
if element.get("__typename") != "LiveEvent":
|
||||
continue # skip ExternalTeaser etc.
|
||||
continue
|
||||
event_id = element.get("id", "")
|
||||
if not event_id or event_id in seen:
|
||||
continue
|
||||
seen.add(event_id)
|
||||
try:
|
||||
events.append(RTLPlusLiveEvent.from_api_node(element))
|
||||
except Exception as e:
|
||||
|
||||
@@ -139,6 +139,10 @@ class RTLPlusVodManager:
|
||||
if category_path[0].startswith(RTLPlusDefaults.VOD_SERIES_ROOT_WATCH_PATH):
|
||||
return self._dispatch_series(category_path)
|
||||
|
||||
# Shows branch — watch-path based IDs
|
||||
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])
|
||||
@@ -191,6 +195,12 @@ class RTLPlusVodManager:
|
||||
provider="rtlplus",
|
||||
description="Serien auf RTL+",
|
||||
),
|
||||
VodCategory(
|
||||
name="Shows",
|
||||
content_id=RTLPlusDefaults.VOD_SHOWS_ROOT_WATCH_PATH,
|
||||
provider="rtlplus",
|
||||
description="Shows auf RTL+",
|
||||
),
|
||||
]
|
||||
results.extend(self._list_topic_worlds())
|
||||
logger.debug(f"RTLPlusVodManager: _list_root() → {len(results)} nodes")
|
||||
@@ -215,10 +225,14 @@ class RTLPlusVodManager:
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_MOVIE,
|
||||
genre_slug=genre_slug,
|
||||
)
|
||||
return self._list_genres(
|
||||
genres = self._list_genres(
|
||||
root_watch_path=RTLPlusDefaults.VOD_MOVIES_ROOT_WATCH_PATH,
|
||||
genre_slugs=RTLPlusDefaults.VOD_MOVIE_GENRE_SLUGS,
|
||||
)
|
||||
items = self._list_overview_page_items(
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_MOVIE,
|
||||
)
|
||||
return genres + items
|
||||
if depth == 2:
|
||||
genre_slug = category_path[1].rsplit("/", 1)[-1]
|
||||
return self._list_overview_page_items(
|
||||
@@ -245,10 +259,14 @@ class RTLPlusVodManager:
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_SERIES,
|
||||
genre_slug=genre_slug,
|
||||
)
|
||||
return self._list_genres(
|
||||
genres = self._list_genres(
|
||||
root_watch_path=RTLPlusDefaults.VOD_SERIES_ROOT_WATCH_PATH,
|
||||
genre_slugs=RTLPlusDefaults.VOD_SERIES_GENRE_SLUGS,
|
||||
)
|
||||
items = self._list_overview_page_items(
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_SERIES,
|
||||
)
|
||||
return genres + items
|
||||
if depth == 2:
|
||||
genre_slug = category_path[1].rsplit("/", 1)[-1]
|
||||
return self._list_overview_page_items(
|
||||
@@ -258,6 +276,39 @@ class RTLPlusVodManager:
|
||||
logger.warning(f"RTLPlusVodManager: series path too deep: {category_path!r}")
|
||||
return []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Shows branch dispatcher
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _dispatch_shows(
|
||||
self, category_path: List[str]
|
||||
) -> List[Union[VodCategory, VodItem]]:
|
||||
depth = len(category_path)
|
||||
if depth == 1:
|
||||
genre_prefix = RTLPlusDefaults.VOD_SHOWS_ROOT_WATCH_PATH + "/genre/"
|
||||
if category_path[0].startswith(genre_prefix):
|
||||
genre_slug = category_path[0][len(genre_prefix):].split("/")[0]
|
||||
return self._list_overview_page_items(
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_SHOW,
|
||||
genre_slug=genre_slug,
|
||||
)
|
||||
genres = self._list_genres(
|
||||
root_watch_path=RTLPlusDefaults.VOD_SHOWS_ROOT_WATCH_PATH,
|
||||
genre_slugs=RTLPlusDefaults.VOD_SHOW_GENRE_SLUGS,
|
||||
)
|
||||
items = self._list_overview_page_items(
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_SHOW,
|
||||
)
|
||||
return genres + items
|
||||
if depth == 2:
|
||||
genre_slug = category_path[1].rsplit("/", 1)[-1]
|
||||
return self._list_overview_page_items(
|
||||
element_type=RTLPlusDefaults.VOD_ELEMENT_TYPE_SHOW,
|
||||
genre_slug=genre_slug,
|
||||
)
|
||||
logger.warning(f"RTLPlusVodManager: shows path too deep: {category_path!r}")
|
||||
return []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Shared genre list (movies + series, depth 1 of each branch)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -323,13 +374,13 @@ class RTLPlusVodManager:
|
||||
def _list_overview_page_items(
|
||||
self,
|
||||
element_type: str,
|
||||
genre_slug: str,
|
||||
genre_slug: Optional[str] = None,
|
||||
) -> List[Union[VodCategory, VodItem]]:
|
||||
"""
|
||||
Paginate OverviewPage for a single genre slug.
|
||||
Paginate OverviewPage for an element type, optionally filtered by genre.
|
||||
|
||||
element_type: RTLPlusDefaults.VOD_ELEMENT_TYPE_MOVIE or …SERIES
|
||||
genre_slug: e.g. "drama", "action"
|
||||
genre_slug: e.g. "drama", "action" — omit for unfiltered (all items)
|
||||
|
||||
Series items at this level are returned as VodCategory nodes (each
|
||||
wraps a Format RRN) so the caller can drill into seasons/episodes via
|
||||
@@ -344,7 +395,7 @@ class RTLPlusVodManager:
|
||||
while True:
|
||||
params = RTLPlusGraphQL.overview_page(
|
||||
element_type=element_type,
|
||||
genres=[genre_slug],
|
||||
genres=[genre_slug] if genre_slug else None,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
@@ -390,7 +441,7 @@ class RTLPlusVodManager:
|
||||
|
||||
logger.debug(
|
||||
f"RTLPlusVodManager: _list_overview_page_items"
|
||||
f"({element_type}, {genre_slug}) → {len(results)} items"
|
||||
f"({element_type}, genre={genre_slug}) → {len(results)} items"
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
Reference in New Issue
Block a user