From 4b5891457e89dc10eb5e7370a073a63d538dca4c Mon Sep 17 00:00:00 2001 From: UrloMythus Date: Sun, 18 May 2025 22:09:07 +0200 Subject: [PATCH] updated to newest version, dlhd support --- mediaflow_proxy/configs.py | 5 +- mediaflow_proxy/extractors/dlhd.py | 335 ++++++++++++++++++++++++ mediaflow_proxy/extractors/factory.py | 2 + mediaflow_proxy/handlers.py | 10 +- mediaflow_proxy/main.py | 2 + mediaflow_proxy/middleware.py | 26 ++ mediaflow_proxy/schemas.py | 12 +- mediaflow_proxy/utils/http_utils.py | 114 ++++++-- mediaflow_proxy/utils/m3u8_processor.py | 4 +- 9 files changed, 486 insertions(+), 24 deletions(-) create mode 100644 mediaflow_proxy/extractors/dlhd.py create mode 100644 mediaflow_proxy/middleware.py diff --git a/mediaflow_proxy/configs.py b/mediaflow_proxy/configs.py index 391ad6f..9e42d13 100644 --- a/mediaflow_proxy/configs.py +++ b/mediaflow_proxy/configs.py @@ -56,9 +56,12 @@ class Settings(BaseSettings): log_level: str = "INFO" # The logging level to use. transport_config: TransportConfig = Field(default_factory=TransportConfig) # Configuration for httpx transport. enable_streaming_progress: bool = False # Whether to enable streaming progress tracking. + disable_home_page: bool = False # Whether to disable the home page UI. + disable_docs: bool = False # Whether to disable the API documentation (Swagger UI). + disable_speedtest: bool = False # Whether to disable the speedtest UI. user_agent: str = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" # The user agent to use for HTTP requests. + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" # The user agent to use for HTTP requests. ) class Config: diff --git a/mediaflow_proxy/extractors/dlhd.py b/mediaflow_proxy/extractors/dlhd.py new file mode 100644 index 0000000..a971f28 --- /dev/null +++ b/mediaflow_proxy/extractors/dlhd.py @@ -0,0 +1,335 @@ +import re +from typing import Dict, Any, Optional +from urllib.parse import urlparse, quote + +from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError + + +class DLHDExtractor(BaseExtractor): + """DLHD (DaddyLive) URL extractor for M3U8 streams.""" + + def __init__(self, request_headers: dict): + super().__init__(request_headers) + # Default to HLS proxy endpoint + self.mediaflow_endpoint = "hls_manifest_proxy" + + async def extract(self, url: str, **kwargs) -> Dict[str, Any]: + """Extract DLHD stream URL and required headers. + + Args: + url: The DaddyLive channel URL (required) + + Keyword Args: + player_url: Direct player URL (optional) + stream_url: The stream URL (optional) + auth_url_base: Base URL for auth requests (optional) + + Returns: + Dict containing stream URL and required headers + """ + try: + # Channel URL is required and serves as the referer + channel_url = url + player_origin = self._get_origin(channel_url) + + # Check for direct parameters + player_url = kwargs.get("player_url") + stream_url = kwargs.get("stream_url") + auth_url_base = kwargs.get("auth_url_base") + + # If player URL not provided, extract it from channel page + if not player_url: + # Get the channel page to extract the player iframe URL + channel_headers = { + "referer": player_origin + "/", + "origin": player_origin, + "user-agent": self.base_headers["user-agent"] + } + + channel_response = await self._make_request(channel_url, headers=channel_headers) + player_url = self._extract_player_url(channel_response.text) + + if not player_url: + raise ExtractorError("Could not extract player URL from channel page") + + # Check if this is a vecloud URL + if "vecloud" in player_url: + return await self._handle_vecloud(player_url, player_origin + "/") + + # Get player page to extract authentication information + player_headers = { + "referer": player_origin + "/", + "origin": player_origin, + "user-agent": self.base_headers["user-agent"] + } + + player_response = await self._make_request(player_url, headers=player_headers) + player_content = player_response.text + + # Extract authentication details from script tag + auth_data = self._extract_auth_data(player_content) + if not auth_data: + raise ExtractorError("Failed to extract authentication data from player") + + # Extract auth URL base if not provided + if not auth_url_base: + auth_url_base = self._extract_auth_url_base(player_content) + + # If still no auth URL base, try to derive from stream URL or player URL + if not auth_url_base: + if stream_url: + auth_url_base = self._get_origin(stream_url) + else: + # Try to extract from player URL structure + player_domain = self._get_origin(player_url) + # Attempt to construct a standard auth domain + auth_url_base = self._derive_auth_url_base(player_domain) + + if not auth_url_base: + raise ExtractorError("Could not determine auth URL base") + + # Construct auth URL + auth_url = (f"{auth_url_base}/auth.php?channel_id={auth_data['channel_key']}" + f"&ts={auth_data['auth_ts']}&rnd={auth_data['auth_rnd']}" + f"&sig={quote(auth_data['auth_sig'])}") + + # Make auth request + player_origin = self._get_origin(player_url) + auth_headers = { + "referer": player_origin + "/", + "origin": player_origin, + "user-agent": self.base_headers["user-agent"] + } + + auth_response = await self._make_request(auth_url, headers=auth_headers) + + # Check if authentication succeeded + if auth_response.json().get("status") != "ok": + raise ExtractorError("Authentication failed") + + # If no stream URL provided, look up the server and generate the stream URL + if not stream_url: + stream_url = await self._lookup_server( + lookup_url_base=player_origin, + auth_url_base=auth_url_base, + auth_data=auth_data, + headers=auth_headers + ) + + # Set up the final stream headers + stream_headers = { + "referer": player_url, + "origin": player_origin, + "user-agent": self.base_headers["user-agent"] + } + + # Return the stream URL with headers + return { + "destination_url": stream_url, + "request_headers": stream_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } + + except Exception as e: + raise ExtractorError(f"Extraction failed: {str(e)}") + + async def _handle_vecloud(self, player_url: str, channel_referer: str) -> Dict[str, Any]: + """Handle vecloud URLs with their specific API. + + Args: + player_url: The vecloud player URL + channel_referer: The referer of the channel page + Returns: + Dict containing stream URL and required headers + """ + try: + # Extract stream ID from vecloud URL + stream_id_match = re.search(r'/stream/([a-zA-Z0-9-]+)', player_url) + if not stream_id_match: + raise ExtractorError("Could not extract stream ID from vecloud URL") + + stream_id = stream_id_match.group(1) + + # Construct API URL + player_parsed = urlparse(player_url) + player_domain = player_parsed.netloc + player_origin = f"{player_parsed.scheme}://{player_parsed.netloc}" + api_url = f"{player_origin}/api/source/{stream_id}?type=live" + + # Set up headers for API request + api_headers = { + "referer": player_url, + "origin": player_origin, + "user-agent": self.base_headers["user-agent"], + "content-type": "application/json" + } + + api_data = { + "r": channel_referer, + "d": player_domain + } + + # Make API request + api_response = await self._make_request(api_url, method="POST", headers=api_headers, json=api_data) + api_data = api_response.json() + + # Check if request was successful + if not api_data.get("success"): + raise ExtractorError("Vecloud API request failed") + + # Extract stream URL from response + stream_url = api_data.get("player", {}).get("source_file") + + if not stream_url: + raise ExtractorError("Could not find stream URL in vecloud response") + + # Set up stream headers + stream_headers = { + "referer": player_origin + "/", + "origin": player_origin, + "user-agent": self.base_headers["user-agent"] + } + + # Return the stream URL with headers + return { + "destination_url": stream_url, + "request_headers": stream_headers, + "mediaflow_endpoint": self.mediaflow_endpoint, + } + + except Exception as e: + raise ExtractorError(f"Vecloud extraction failed: {str(e)}") + + def _extract_player_url(self, html_content: str) -> Optional[str]: + """Extract player iframe URL from channel page HTML.""" + try: + # Look for iframe with allowfullscreen attribute + iframe_match = re.search( + r']*src=["\']([^"\']+)["\'][^>]*allowfullscreen', + html_content, + re.IGNORECASE + ) + + if not iframe_match: + # Try alternative pattern without requiring allowfullscreen + iframe_match = re.search( + r']*src=["\']([^"\']+(?:premiumtv|daddylivehd|vecloud)[^"\']*)["\']', + html_content, + re.IGNORECASE + ) + + if iframe_match: + return iframe_match.group(1).strip() + + return None + except Exception: + return None + + async def _lookup_server(self, lookup_url_base: str, auth_url_base: str, auth_data: Dict[str, str], headers: Dict[str, str]) -> str: + """Lookup server information and generate stream URL.""" + try: + # Construct server lookup URL + server_lookup_url = f"{lookup_url_base}/server_lookup.php?channel_id={quote(auth_data['channel_key'])}" + + # Make server lookup request + server_response = await self._make_request( + server_lookup_url, + headers=headers + ) + + server_data = server_response.json() + server_key = server_data.get("server_key") + + if not server_key: + raise ExtractorError("Failed to get server key") + + # Extract domain parts from auth URL for constructing stream URL + auth_domain_parts = urlparse(auth_url_base).netloc.split('.') + domain_suffix = '.'.join(auth_domain_parts[1:]) if len(auth_domain_parts) > 1 else auth_domain_parts[0] + + # Generate the m3u8 URL based on server response pattern + if '/' in server_key: + # Handle special case like "top1/cdn" + parts = server_key.split('/') + return f"https://{parts[0]}.{domain_suffix}/{server_key}/{auth_data['channel_key']}/mono.m3u8" + else: + # Handle normal case + return f"https://{server_key}new.{domain_suffix}/{server_key}/{auth_data['channel_key']}/mono.m3u8" + + except Exception as e: + raise ExtractorError(f"Server lookup failed: {str(e)}") + + def _extract_auth_data(self, html_content: str) -> Dict[str, str]: + """Extract authentication data from player page.""" + try: + # Extract channel key + channel_key_match = re.search(r'var\s+channelKey\s*=\s*["\']([^"\']+)["\']', html_content) + # Extract auth timestamp + auth_ts_match = re.search(r'var\s+authTs\s*=\s*["\']([^"\']+)["\']', html_content) + # Extract auth random value + auth_rnd_match = re.search(r'var\s+authRnd\s*=\s*["\']([^"\']+)["\']', html_content) + # Extract auth signature + auth_sig_match = re.search(r'var\s+authSig\s*=\s*["\']([^"\']+)["\']', html_content) + + if not all([channel_key_match, auth_ts_match, auth_rnd_match, auth_sig_match]): + return {} + + return { + "channel_key": channel_key_match.group(1), + "auth_ts": auth_ts_match.group(1), + "auth_rnd": auth_rnd_match.group(1), + "auth_sig": auth_sig_match.group(1) + } + except Exception: + return {} + + def _extract_auth_url_base(self, html_content: str) -> Optional[str]: + """Extract auth URL base from player page script content.""" + try: + # Look for auth URL or domain in fetchWithRetry call or similar patterns + auth_url_match = re.search( + r'fetchWithRetry\([\'"]([^\'"]*/auth\.php)', + html_content + ) + + if auth_url_match: + auth_url = auth_url_match.group(1) + # Extract base URL up to the auth.php part + return auth_url.split('/auth.php')[0] + + # Try finding domain directly + domain_match = re.search( + r'[\'"]https://([^/\'\"]+)(?:/[^\'\"]*)?/auth\.php', + html_content + ) + + if domain_match: + return f"https://{domain_match.group(1)}" + + return None + except Exception: + return None + + def _get_origin(self, url: str) -> str: + """Extract origin from URL.""" + parsed = urlparse(url) + return f"{parsed.scheme}://{parsed.netloc}" + + def _derive_auth_url_base(self, player_domain: str) -> Optional[str]: + """Attempt to derive auth URL base from player domain.""" + try: + # Typical pattern is to use a subdomain for auth domain + parsed = urlparse(player_domain) + domain_parts = parsed.netloc.split('.') + + # Get the top-level domain and second-level domain + if len(domain_parts) >= 2: + base_domain = '.'.join(domain_parts[-2:]) + # Try common subdomains for auth + for prefix in ['auth', 'api', 'cdn']: + potential_auth_domain = f"https://{prefix}.{base_domain}" + return potential_auth_domain + + return None + except Exception: + return None diff --git a/mediaflow_proxy/extractors/factory.py b/mediaflow_proxy/extractors/factory.py index 09855df..8afa9cf 100644 --- a/mediaflow_proxy/extractors/factory.py +++ b/mediaflow_proxy/extractors/factory.py @@ -1,6 +1,7 @@ from typing import Dict, Type from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError +from mediaflow_proxy.extractors.dlhd import DLHDExtractor from mediaflow_proxy.extractors.doodstream import DoodStreamExtractor from mediaflow_proxy.extractors.livetv import LiveTVExtractor from mediaflow_proxy.extractors.maxstream import MaxstreamExtractor @@ -25,6 +26,7 @@ class ExtractorFactory: "Okru": OkruExtractor, "Maxstream": MaxstreamExtractor, "LiveTV": LiveTVExtractor, + "DLHD": DLHDExtractor, } @classmethod diff --git a/mediaflow_proxy/handlers.py b/mediaflow_proxy/handlers.py index 882e975..97aeafe 100644 --- a/mediaflow_proxy/handlers.py +++ b/mediaflow_proxy/handlers.py @@ -76,10 +76,10 @@ async def handle_hls_stream_proxy( Returns: Union[Response, EnhancedStreamingResponse]: Either a processed m3u8 playlist or a streaming response. """ - client, streamer = await setup_client_and_streamer() + _, streamer = await setup_client_and_streamer() # Handle range requests content_range = proxy_headers.request.get("range", "bytes=0-") - if "NaN" in content_range: + if "nan" in content_range.casefold(): # Handle invalid range requests "bytes=NaN-NaN" raise HTTPException(status_code=416, detail="Invalid Range Header") proxy_headers.request.update({"range": content_range}) @@ -213,9 +213,9 @@ async def fetch_and_process_m3u8( # Initialize processor and response headers processor = M3U8Processor(request, key_url) response_headers = { - "Content-Disposition": "inline", - "Accept-Ranges": "none", - "Content-Type": "application/vnd.apple.mpegurl", + "content-disposition": "inline", + "accept-ranges": "none", + "content-type": "application/vnd.apple.mpegurl", } response_headers.update(proxy_headers.response) diff --git a/mediaflow_proxy/main.py b/mediaflow_proxy/main.py index 4fe19c2..8156601 100644 --- a/mediaflow_proxy/main.py +++ b/mediaflow_proxy/main.py @@ -9,6 +9,7 @@ from starlette.responses import RedirectResponse from starlette.staticfiles import StaticFiles from mediaflow_proxy.configs import settings +from mediaflow_proxy.middleware import UIAccessControlMiddleware from mediaflow_proxy.routes import proxy_router, extractor_router, speedtest_router from mediaflow_proxy.schemas import GenerateUrlRequest, GenerateMultiUrlRequest, MultiUrlRequestItem from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware @@ -26,6 +27,7 @@ app.add_middleware( allow_headers=["*"], ) app.add_middleware(EncryptionMiddleware) +app.add_middleware(UIAccessControlMiddleware) async def verify_api_key(api_key: str = Security(api_password_query), api_key_alt: str = Security(api_password_header)): diff --git a/mediaflow_proxy/middleware.py b/mediaflow_proxy/middleware.py new file mode 100644 index 0000000..d217587 --- /dev/null +++ b/mediaflow_proxy/middleware.py @@ -0,0 +1,26 @@ +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware + +from mediaflow_proxy.configs import settings + + +class UIAccessControlMiddleware(BaseHTTPMiddleware): + """Middleware that controls access to UI components based on settings.""" + + async def dispatch(self, request: Request, call_next): + path = request.url.path + + # Block access to home page + if settings.disable_home_page and (path == "/" or path == "/index.html"): + return Response(status_code=403, content="Forbidden") + + # Block access to API docs + if settings.disable_docs and (path == "/docs" or path == "/redoc" or path.startswith("/openapi")): + return Response(status_code=403, content="Forbidden") + + # Block access to speedtest UI + if settings.disable_speedtest and path.startswith("/speedtest"): + return Response(status_code=403, content="Forbidden") + + return await call_next(request) + diff --git a/mediaflow_proxy/schemas.py b/mediaflow_proxy/schemas.py index 1ffee30..9883883 100644 --- a/mediaflow_proxy/schemas.py +++ b/mediaflow_proxy/schemas.py @@ -1,6 +1,7 @@ +import json from typing import Literal, Dict, Any, Optional -from pydantic import BaseModel, Field, IPvAnyAddress, ConfigDict +from pydantic import BaseModel, Field, IPvAnyAddress, ConfigDict, field_validator class GenerateUrlRequest(BaseModel): @@ -88,7 +89,7 @@ class MPDSegmentParams(GenericParams): class ExtractorURLParams(GenericParams): host: Literal[ - "Doodstream", "Mixdrop", "Uqload", "Streamtape", "Supervideo", "VixCloud", "Okru", "Maxstream", "LiveTV" + "Doodstream", "Mixdrop", "Uqload", "Streamtape", "Supervideo", "VixCloud", "Okru", "Maxstream", "LiveTV", "DLHD" ] = Field(..., description="The host to extract the URL from.") destination: str = Field(..., description="The URL of the stream.", alias="d") redirect_stream: bool = Field(False, description="Whether to redirect to the stream endpoint automatically.") @@ -96,3 +97,10 @@ class ExtractorURLParams(GenericParams): default_factory=dict, description="Additional parameters required for specific extractors (e.g., stream_title for LiveTV)", ) + + + @field_validator("extra_params", mode="before") + def validate_extra_params(cls, value: Any): + if isinstance(value, str): + return json.loads(value) + return value diff --git a/mediaflow_proxy/utils/http_utils.py b/mediaflow_proxy/utils/http_utils.py index 6bb9ba0..76cfe00 100644 --- a/mediaflow_proxy/utils/http_utils.py +++ b/mediaflow_proxy/utils/http_utils.py @@ -6,6 +6,7 @@ from urllib import parse from urllib.parse import urlencode import anyio +import h11 import httpx import tenacity from fastapi import Response @@ -168,6 +169,20 @@ class Streamer: except httpx.TimeoutException: logger.warning("Timeout while streaming") raise DownloadError(409, "Timeout while streaming") + except httpx.RemoteProtocolError as e: + # Special handling for connection closed errors + if "peer closed connection without sending complete message body" in str(e): + logger.warning(f"Remote server closed connection prematurely: {e}") + # If we've received some data, just log the warning and return normally + if self.bytes_transferred > 0: + logger.info(f"Partial content received ({self.bytes_transferred} bytes). Continuing with available data.") + return + else: + # If we haven't received any data, raise an error + raise DownloadError(502, f"Remote server closed connection without sending any data: {e}") + else: + logger.error(f"Protocol error while streaming: {e}") + raise DownloadError(502, f"Protocol error while streaming: {e}") except GeneratorExit: logger.info("Streaming session stopped by the user") except Exception as e: @@ -432,6 +447,7 @@ class EnhancedStreamingResponse(Response): self.media_type = self.media_type if media_type is None else media_type self.background = background self.init_headers(headers) + self.actual_content_length = 0 @staticmethod async def listen_for_disconnect(receive: Receive) -> None: @@ -446,41 +462,109 @@ class EnhancedStreamingResponse(Response): async def stream_response(self, send: Send) -> None: try: + # Initialize headers + headers = list(self.raw_headers) + + # Set the transfer-encoding to chunked for streamed responses with content-length + # when content-length is present. This ensures we don't hit protocol errors + # if the upstream connection is closed prematurely. + for i, (name, _) in enumerate(headers): + if name.lower() == b"content-length": + # Replace content-length with transfer-encoding: chunked for streaming + headers[i] = (b"transfer-encoding", b"chunked") + headers = [h for h in headers if h[0].lower() != b"content-length"] + logger.debug("Switched from content-length to chunked transfer-encoding for streaming") + break + + # Start the response await send( { "type": "http.response.start", "status": self.status_code, - "headers": self.raw_headers, + "headers": headers, } ) - async for chunk in self.body_iterator: - if not isinstance(chunk, (bytes, memoryview)): - chunk = chunk.encode(self.charset) - try: - await send({"type": "http.response.body", "body": chunk, "more_body": True}) - except (ConnectionResetError, anyio.BrokenResourceError): - logger.info("Client disconnected during streaming") - return - await send({"type": "http.response.body", "body": b"", "more_body": False}) + # Track if we've sent any data + data_sent = False + + try: + async for chunk in self.body_iterator: + if not isinstance(chunk, (bytes, memoryview)): + chunk = chunk.encode(self.charset) + try: + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + data_sent = True + self.actual_content_length += len(chunk) + except (ConnectionResetError, anyio.BrokenResourceError): + logger.info("Client disconnected during streaming") + return + + # Successfully streamed all content + await send({"type": "http.response.body", "body": b"", "more_body": False}) + except (httpx.RemoteProtocolError, h11._util.LocalProtocolError) as e: + # Handle connection closed errors + if data_sent: + # We've sent some data to the client, so try to complete the response + logger.warning(f"Remote protocol error after partial streaming: {e}") + try: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + logger.info(f"Response finalized after partial content ({self.actual_content_length} bytes transferred)") + except Exception as close_err: + logger.warning(f"Could not finalize response after remote error: {close_err}") + else: + # No data was sent, re-raise the error + logger.error(f"Protocol error before any data was streamed: {e}") + raise except Exception as e: logger.exception(f"Error in stream_response: {str(e)}") + if not isinstance(e, (ConnectionResetError, anyio.BrokenResourceError)): + try: + # Try to send an error response if client is still connected + await send( + { + "type": "http.response.start", + "status": 502, + "headers": [(b"content-type", b"text/plain")], + } + ) + error_message = f"Streaming error: {str(e)}".encode("utf-8") + await send({"type": "http.response.body", "body": error_message, "more_body": False}) + except Exception: + # If we can't send an error response, just log it + pass async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: async with anyio.create_task_group() as task_group: + streaming_completed = False + stream_func = partial(self.stream_response, send) + listen_func = partial(self.listen_for_disconnect, receive) async def wrap(func: typing.Callable[[], typing.Awaitable[None]]) -> None: try: await func() + # If this is the stream_response function and it completes successfully, mark as done + if func == stream_func: + nonlocal streaming_completed + streaming_completed = True except Exception as e: - if not isinstance(e, anyio.get_cancelled_exc_class()): + if isinstance(e, (httpx.RemoteProtocolError, h11._util.LocalProtocolError)): + # Handle protocol errors more gracefully + logger.warning(f"Protocol error during streaming: {e}") + elif not isinstance(e, anyio.get_cancelled_exc_class()): logger.exception("Error in streaming task") - raise + # Only re-raise if it's not a protocol error or cancellation + raise finally: - task_group.cancel_scope.cancel() + # Only cancel the task group if we're in disconnect listener or + # if streaming_completed is True (meaning we finished normally) + if func == listen_func or streaming_completed: + task_group.cancel_scope.cancel() - task_group.start_soon(wrap, partial(self.stream_response, send)) - await wrap(partial(self.listen_for_disconnect, receive)) + # Start the streaming response in a separate task + task_group.start_soon(wrap, stream_func) + # Listen for disconnect events + await wrap(listen_func) if self.background is not None: await self.background() diff --git a/mediaflow_proxy/utils/m3u8_processor.py b/mediaflow_proxy/utils/m3u8_processor.py index 87e0f6a..1c6e743 100644 --- a/mediaflow_proxy/utils/m3u8_processor.py +++ b/mediaflow_proxy/utils/m3u8_processor.py @@ -143,11 +143,13 @@ class M3U8Processor: full_url = parse.urljoin(base_url, url) query_params = dict(self.request.query_params) has_encrypted = query_params.pop("has_encrypted", False) + # Remove the response headers from the query params to avoid it being added to the consecutive requests + [query_params.pop(key, None) for key in list(query_params.keys()) if key.startswith("r_")] return encode_mediaflow_proxy_url( self.mediaflow_proxy_url, "", full_url, - query_params=dict(self.request.query_params), + query_params=query_params, encryption_handler=encryption_handler if has_encrypted else None, )