Add discovery

This commit is contained in:
Nirvana
2026-03-17 20:09:46 +01:00
parent d6d9ffcb91
commit c81dec6d5b
5 changed files with 69 additions and 55 deletions
@@ -114,6 +114,12 @@ class VodCategory:
# Stored for convenience / debugging; the content_id already encodes the path.
details_url: Optional[str] = None
# Full URL to use when fetching this category's children, including any
# portal-scoping query params (e.g. ?whiteLabelId=megathek). When present,
# get_vod_category / _fetch_lane_items should use this URL directly instead
# of reconstructing it from content_id, which loses query params.
fetch_url: Optional[str] = None
# Cached slug (computed lazily if not set)
_slug: Optional[str] = field(default=None, repr=False)
@@ -138,6 +144,7 @@ class VodCategory:
"description": self.description,
"child_count": self.child_count,
"details_url": self.details_url,
"fetch_url": self.fetch_url,
}
+8 -1
View File
@@ -651,7 +651,10 @@ class StreamingProvider(ABC):
return False
def get_vod_category(
self, category_path: List[str], **kwargs
self,
category_path: List[str],
fetch_url: Optional[str] = None,
**kwargs
) -> List:
"""
Return the children of a VOD tree node.
@@ -665,6 +668,10 @@ class StreamingProvider(ABC):
The provider typically only needs category_path[-1]
(the immediate parent id), but the full path is
provided for providers that require ancestor context.
fetch_url: Full URL (including portal-scoping query params such
as ?whiteLabelId=megathek) to use for the fetch.
Populated from VodCategory.fetch_url by the caller so
that query params survive HTTP router path splitting.
Returns:
Mixed list of VodCategory and VodItem objects.
@@ -70,6 +70,7 @@ class VodOperations:
self,
provider_name: str,
slug_segments: List[str],
**kwargs,
) -> List[Union[VodCategory, VodItem]]:
"""
Resolve a slug path and return the children of that node.
@@ -103,7 +104,12 @@ class VodOperations:
# The old slug-walking approach (_resolve_path_to_ids) is bypassed
# because it fetches every intermediate level unnecessarily and fails
# when the provider's tree is too deep or slugs don't match exactly.
children = provider.get_vod_category(slug_segments)
#
# fetch_url: passed through from the VodCategory returned at the
# previous level so that portal-scoping query params (e.g.
# ?whiteLabelId=megathek) survive the HTTP router's path splitting.
fetch_url = kwargs.pop("fetch_url", None)
children = provider.get_vod_category(slug_segments, fetch_url=fetch_url)
logger.info(
f"Retrieved {len(children)} VOD entries from '{provider_name}' "
@@ -1035,10 +1035,14 @@ class Magenta2Provider(StreamingProvider):
"accept-encoding": "gzip",
}
def get_vod_category(self, category_path, **kwargs):
def get_vod_category(self, category_path, fetch_url=None, **kwargs):
if not self._vod_manager:
raise RuntimeError("VodManager not available - configuration discovery may have failed")
return self._vod_manager.get_children(category_path=category_path, **kwargs)
return self._vod_manager.get_children(
category_path=category_path,
fetch_url=fetch_url,
**kwargs,
)
def enrich_channel_data(
self, channel: StreamingChannel, **kwargs
@@ -127,11 +127,6 @@ class VodManager:
_platform = getattr(bootstrap, "platform", "")
self._subscriber_type: str = SUBSCRIBER_TYPES.get(_platform, "FTV_OTT_DT")
# Mapping of UnstructuredGrid flex_id → extra query params (e.g. whiteLabelId)
# populated when lanes are parsed so that the params survive the caller's
# path-splitting which strips query strings from content_id segments.
self._lane_extra_params: Dict[str, Dict[str, str]] = {}
# Short-lived in-memory cache for VodDetails responses (content_id → data).
# Prevents redundant network round-trips when the same content_id is
# looked up multiple times within a single get_children() call chain
@@ -151,6 +146,7 @@ class VodManager:
*,
page_size: int = VOD_DEFAULT_PAGE_SIZE,
offset: int = 0,
fetch_url: Optional[str] = None,
) -> List[Union[VodCategory, VodItem]]:
"""
Return the children of a VOD tree node.
@@ -163,44 +159,34 @@ class VodManager:
["GN_SERIES_123",
"GN_SEASON_123_DE_2"] -> season (episodes)
["lane:326619"] -> UnstructuredGrid lane
page_size: Number of items to fetch per page (UnstructuredGrid).
offset: Pagination offset (UnstructuredGrid).
page_size: Number of items to fetch per page (UnstructuredGrid).
offset: Pagination offset (UnstructuredGrid).
fetch_url: Full URL (including portal-scoping query params such as
?whiteLabelId=megathek) to use for the fetch instead of
reconstructing it from category_path. Populated by the
caller from VodCategory.fetch_url so that params survive
the HTTP router's path-segment splitting.
Returns:
Mixed list of VodCategory and VodItem objects.
"""
logger.debug(
f"{self._provider}: get_children called with category_path={category_path!r}"
f"{self._provider}: get_children called with "
f"category_path={category_path!r} fetch_url={fetch_url!r}"
)
params = self._base_params()
if not category_path:
return self._fetch_home_lanes(params)
# Reconstruct the full content_id from the path segments.
# The caller splits on "/" so "UnstructuredGrid/357162" arrives as
# ["UnstructuredGrid", "357162"] and "VodDetails/202887/GN_SERIES_9370385"
# arrives as ["VodDetails", "202887", "GN_SERIES_9370385"].
node_id = "/".join(category_path)
if node_id.startswith("UnstructuredGrid/"):
# The tail after "UnstructuredGrid/" may include query params
# (e.g. "357162?whiteLabelId=megathek") that were preserved from
# the original laneContentLink. Split them out so _fetch_lane_items
# can pass them as extra params rather than having them corrupt the
# flex_id path segment.
tail = node_id[len("UnstructuredGrid/"):]
if "?" in tail:
flex_id, qs_string = tail.split("?", 1)
from urllib.parse import parse_qs
extra = {k: v[0] for k, v in parse_qs(qs_string).items()}
else:
flex_id = tail
extra = {}
flex_id = node_id[len("UnstructuredGrid/"):].split("?")[0]
return self._fetch_lane_items(
flex_id, params,
page_size=page_size, offset=offset,
extra_params=extra,
fetch_url=fetch_url,
)
if node_id.startswith("VodDetails/"):
@@ -700,16 +686,10 @@ class VodManager:
f"{self._provider}: Lane '{title}' → content_id={content_id!r}"
)
# Store any query params (e.g. whiteLabelId) keyed by bare flex_id so
# _fetch_lane_items can recover them even after the caller strips the
# query string from the content_id path segments.
bare_flex_id = content_id.split("?")[0].split("/")[-1]
if "?" in content_id:
from urllib.parse import parse_qs
stored_qs = parse_qs(content_id.split("?", 1)[1])
self._lane_extra_params[bare_flex_id] = {
k: v[0] for k, v in stored_qs.items()
}
# Determine the best fetch_url for this lane — the full URL including
# ?whiteLabelId=... that the caller passes back via VodCategory.fetch_url.
# Priority: showAllUrl (paginated full grid) > laneContentLink (always present).
fetch_url = show_all_href or lane_content_href or None
categories.append(
VodCategory(
@@ -718,6 +698,7 @@ class VodManager:
provider=self._provider,
child_count=lane.get("totalCount"),
details_url=show_all_href,
fetch_url=fetch_url,
)
)
@@ -734,32 +715,41 @@ class VodManager:
params: Dict,
page_size: int = VOD_DEFAULT_PAGE_SIZE,
offset: int = 0,
extra_params: Optional[Dict] = None,
fetch_url: Optional[str] = None,
) -> List[Union[VodCategory, VodItem]]:
"""
Fetch items from an UnstructuredGrid lane. Movies → VodItem,
series → VodCategory (seasons/episodes require further drill-down).
Args:
extra_params: Additional query parameters extracted from the
content_id (e.g. {'whiteLabelId': 'megathek'}).
These originate from the server-supplied laneContentLink
and must be forwarded verbatim so the correct portal
scope is applied.
fetch_url: Full URL supplied by the caller from VodCategory.fetch_url,
including portal-scoping query params (e.g. ?whiteLabelId=megathek).
When present this is used as-is (base URL only, params merged
separately); when absent the URL is constructed from flex_id.
"""
url = f"{self._base_url()}/UnstructuredGrid/{flex_id}"
from urllib.parse import urlparse, parse_qs, urlunparse
if fetch_url:
# Use the base path from fetch_url but merge its query params into
# paged_params so pagination ($size, $offset) and auth params are
# all sent together in one clean params dict.
parsed = urlparse(fetch_url)
url = urlunparse(parsed._replace(query=""))
url_params = {k: v[0] for k, v in parse_qs(parsed.query).items()}
else:
url = f"{self._base_url()}/UnstructuredGrid/{flex_id}"
url_params = {}
paged_params = dict(params)
# Merge caller-supplied extra params first, then overlay with any
# params stored at lane-parse time (keyed by bare flex_id). The
# stored params survive the caller's query-string stripping.
if extra_params:
paged_params.update(extra_params)
stored = self._lane_extra_params.get(flex_id)
if stored:
paged_params.update(stored)
if url_params:
paged_params.update(url_params)
paged_params["$size"] = str(page_size)
paged_params["$offset"] = str(offset)
logger.debug(
f"{self._provider}: _fetch_lane_items flex_id={flex_id!r} "
f"params={paged_params!r}"
)
data = self._get(url, paged_params)
if not data:
return []