From 619ed9d5bf9f4f2c8b21c5a3c377e1ad50e979dc Mon Sep 17 00:00:00 2001 From: Nirvana Date: Thu, 5 Mar 2026 17:00:19 +0100 Subject: [PATCH] Add discovery --- .../base/utils/mpd_rewriter.py | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/streaming_providers/base/utils/mpd_rewriter.py b/lib/streaming_providers/base/utils/mpd_rewriter.py index 5b87e49..98a2381 100644 --- a/lib/streaming_providers/base/utils/mpd_rewriter.py +++ b/lib/streaming_providers/base/utils/mpd_rewriter.py @@ -528,7 +528,7 @@ class MPDRewriter: period_base_elem = period.find("mpd:BaseURL", self.MPD_NAMESPACE) if period_base_elem is not None and period_base_elem.text: period_base_text = period_base_elem.text.strip() - period_base_url = urljoin(mpd_base_url, period_base_text) + period_base_url = self._urljoin_preserve_query(mpd_base_url, period_base_text) logger.debug(f"Period {period_id} BaseURL: {period_base_url}") # Remove Period-level BaseURL elements @@ -547,7 +547,7 @@ class MPDRewriter: as_base_elem = adaptation_set.find("mpd:BaseURL", self.MPD_NAMESPACE) if as_base_elem is not None and as_base_elem.text: as_base_text = as_base_elem.text.strip() - as_base_url = urljoin(period_base_url, as_base_text) + as_base_url = self._urljoin_preserve_query(period_base_url, as_base_text) logger.debug(f"AdaptationSet {unique_id} BaseURL: {as_base_url}") # Store the resolved base URL for this AdaptationSet @@ -838,7 +838,7 @@ class MPDRewriter: parent_rep_id = current_rep_id # captured when we entered Representation raw_url = element.text.strip() if raw_url: - resolved = urljoin(base_url, raw_url) + resolved = self._urljoin_preserve_query(base_url, raw_url) # Rewrite the BaseURL text through the proxy. # We don't know the segment type yet (the SegmentBase child will # handle Initialization specifically), so pass None for seg_type @@ -894,7 +894,7 @@ class MPDRewriter: if not val: continue - resolved = urljoin(base_url, val) + resolved = self._urljoin_preserve_query(base_url, val) if "$" in resolved: # Use shared utility for splitting template URLs path, pattern = URLResolver.split_template_url(resolved) @@ -910,7 +910,7 @@ class MPDRewriter: # Handle SegmentURL (always 'media' type) if element.tag.endswith("SegmentURL") and "media" in element.attrib: - resolved = urljoin(base_url, element.attrib["media"]) + resolved = self._urljoin_preserve_query(base_url, element.attrib["media"]) path, pattern = ( URLResolver.split_template_url(resolved) if "$" in resolved @@ -928,6 +928,49 @@ class MPDRewriter: current_encrypted, current_kid, current_period_id, best_video_info ) + @staticmethod + def _urljoin_preserve_query(base: str, url: str) -> str: + """ + Like urljoin(base, url) but preserves the query string of *base* when + the relative *url* itself carries no query string. + + Why this is needed: + Some CDNs (e.g. Discovery+/Max) embed the auth token in the manifest + URL's query string (e.g. ?manifest-params=…). The segment paths + inside such manifests are plain relative paths like "v/0_9cd1a5/v13.mp4" + with no query component. Standard urljoin() discards the base query + string when joining with a relative path (RFC 3986 §5.3 – correct + behaviour in general, wrong here). The CDN requires the same token on + every segment request, so we must re-append it. + + Rules (mirrors RFC 3986 §5.3 reference-resolution): + - If *url* is absolute (has a scheme) → plain urljoin, no query merging. + - If *url* already has its own query string → plain urljoin; the + relative URL's query takes full precedence. + - If *url* has no query string and *base* does → urljoin then re-attach + base's query string to the result. + """ + from urllib.parse import urlparse, urlunparse + + parsed_url = urlparse(url) + + # Absolute URL or url already has its own query — standard behaviour. + if parsed_url.scheme or parsed_url.query: + return urljoin(base, url) + + joined = urljoin(base, url) + + # Re-attach the base query string if it has one and the result lost it. + base_query = urlparse(base).query + if base_query: + parsed_joined = urlparse(joined) + if not parsed_joined.query: + # urlunparse is typed as returning str | bytes depending on input; + # our inputs are always str so cast to make the type checker happy. + joined = str(urlunparse(parsed_joined._replace(query=base_query))) + + return joined + def _extract_mpd_base_url(self, root: ET.Element, manifest_url: str) -> str: """ Extract and resolve MPD-level BaseURL.