diff --git a/lib/streaming_providers/base/models/vod.py b/lib/streaming_providers/base/models/vod.py index 822c654..950901a 100644 --- a/lib/streaming_providers/base/models/vod.py +++ b/lib/streaming_providers/base/models/vod.py @@ -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, } diff --git a/lib/streaming_providers/base/provider.py b/lib/streaming_providers/base/provider.py index 714eb77..8edd3fb 100644 --- a/lib/streaming_providers/base/provider.py +++ b/lib/streaming_providers/base/provider.py @@ -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. diff --git a/lib/streaming_providers/base/vod_operations.py b/lib/streaming_providers/base/vod_operations.py index 5020cf6..3491d0b 100644 --- a/lib/streaming_providers/base/vod_operations.py +++ b/lib/streaming_providers/base/vod_operations.py @@ -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}' " diff --git a/lib/streaming_providers/providers/magenta2/provider.py b/lib/streaming_providers/providers/magenta2/provider.py index c077262..cc1fbb2 100644 --- a/lib/streaming_providers/providers/magenta2/provider.py +++ b/lib/streaming_providers/providers/magenta2/provider.py @@ -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 diff --git a/lib/streaming_providers/providers/magenta2/vod_manager.py b/lib/streaming_providers/providers/magenta2/vod_manager.py index 71eaf98..5a1c0e2 100644 --- a/lib/streaming_providers/providers/magenta2/vod_manager.py +++ b/lib/streaming_providers/providers/magenta2/vod_manager.py @@ -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 []