updated to newest version, dlhd support

This commit is contained in:
UrloMythus
2025-05-18 22:09:07 +02:00
parent 1bdfe198b5
commit 4b5891457e
9 changed files with 486 additions and 24 deletions

View File

@@ -56,9 +56,12 @@ class Settings(BaseSettings):
log_level: str = "INFO" # The logging level to use. log_level: str = "INFO" # The logging level to use.
transport_config: TransportConfig = Field(default_factory=TransportConfig) # Configuration for httpx transport. transport_config: TransportConfig = Field(default_factory=TransportConfig) # Configuration for httpx transport.
enable_streaming_progress: bool = False # Whether to enable streaming progress tracking. 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 = ( 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: class Config:

View File

@@ -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'<iframe[^>]*src=["\']([^"\']+)["\'][^>]*allowfullscreen',
html_content,
re.IGNORECASE
)
if not iframe_match:
# Try alternative pattern without requiring allowfullscreen
iframe_match = re.search(
r'<iframe[^>]*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

View File

@@ -1,6 +1,7 @@
from typing import Dict, Type from typing import Dict, Type
from mediaflow_proxy.extractors.base import BaseExtractor, ExtractorError 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.doodstream import DoodStreamExtractor
from mediaflow_proxy.extractors.livetv import LiveTVExtractor from mediaflow_proxy.extractors.livetv import LiveTVExtractor
from mediaflow_proxy.extractors.maxstream import MaxstreamExtractor from mediaflow_proxy.extractors.maxstream import MaxstreamExtractor
@@ -25,6 +26,7 @@ class ExtractorFactory:
"Okru": OkruExtractor, "Okru": OkruExtractor,
"Maxstream": MaxstreamExtractor, "Maxstream": MaxstreamExtractor,
"LiveTV": LiveTVExtractor, "LiveTV": LiveTVExtractor,
"DLHD": DLHDExtractor,
} }
@classmethod @classmethod

View File

@@ -76,10 +76,10 @@ async def handle_hls_stream_proxy(
Returns: Returns:
Union[Response, EnhancedStreamingResponse]: Either a processed m3u8 playlist or a streaming response. 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 # Handle range requests
content_range = proxy_headers.request.get("range", "bytes=0-") 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" # Handle invalid range requests "bytes=NaN-NaN"
raise HTTPException(status_code=416, detail="Invalid Range Header") raise HTTPException(status_code=416, detail="Invalid Range Header")
proxy_headers.request.update({"range": content_range}) proxy_headers.request.update({"range": content_range})
@@ -213,9 +213,9 @@ async def fetch_and_process_m3u8(
# Initialize processor and response headers # Initialize processor and response headers
processor = M3U8Processor(request, key_url) processor = M3U8Processor(request, key_url)
response_headers = { response_headers = {
"Content-Disposition": "inline", "content-disposition": "inline",
"Accept-Ranges": "none", "accept-ranges": "none",
"Content-Type": "application/vnd.apple.mpegurl", "content-type": "application/vnd.apple.mpegurl",
} }
response_headers.update(proxy_headers.response) response_headers.update(proxy_headers.response)

View File

@@ -9,6 +9,7 @@ from starlette.responses import RedirectResponse
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from mediaflow_proxy.configs import settings 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.routes import proxy_router, extractor_router, speedtest_router
from mediaflow_proxy.schemas import GenerateUrlRequest, GenerateMultiUrlRequest, MultiUrlRequestItem from mediaflow_proxy.schemas import GenerateUrlRequest, GenerateMultiUrlRequest, MultiUrlRequestItem
from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware
@@ -26,6 +27,7 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.add_middleware(EncryptionMiddleware) 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)): async def verify_api_key(api_key: str = Security(api_password_query), api_key_alt: str = Security(api_password_header)):

View File

@@ -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)

View File

@@ -1,6 +1,7 @@
import json
from typing import Literal, Dict, Any, Optional 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): class GenerateUrlRequest(BaseModel):
@@ -88,7 +89,7 @@ class MPDSegmentParams(GenericParams):
class ExtractorURLParams(GenericParams): class ExtractorURLParams(GenericParams):
host: Literal[ 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.") ] = Field(..., description="The host to extract the URL from.")
destination: str = Field(..., description="The URL of the stream.", alias="d") 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.") redirect_stream: bool = Field(False, description="Whether to redirect to the stream endpoint automatically.")
@@ -96,3 +97,10 @@ class ExtractorURLParams(GenericParams):
default_factory=dict, default_factory=dict,
description="Additional parameters required for specific extractors (e.g., stream_title for LiveTV)", 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

View File

@@ -6,6 +6,7 @@ from urllib import parse
from urllib.parse import urlencode from urllib.parse import urlencode
import anyio import anyio
import h11
import httpx import httpx
import tenacity import tenacity
from fastapi import Response from fastapi import Response
@@ -168,6 +169,20 @@ class Streamer:
except httpx.TimeoutException: except httpx.TimeoutException:
logger.warning("Timeout while streaming") logger.warning("Timeout while streaming")
raise DownloadError(409, "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: except GeneratorExit:
logger.info("Streaming session stopped by the user") logger.info("Streaming session stopped by the user")
except Exception as e: 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.media_type = self.media_type if media_type is None else media_type
self.background = background self.background = background
self.init_headers(headers) self.init_headers(headers)
self.actual_content_length = 0
@staticmethod @staticmethod
async def listen_for_disconnect(receive: Receive) -> None: async def listen_for_disconnect(receive: Receive) -> None:
@@ -446,41 +462,109 @@ class EnhancedStreamingResponse(Response):
async def stream_response(self, send: Send) -> None: async def stream_response(self, send: Send) -> None:
try: 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( await send(
{ {
"type": "http.response.start", "type": "http.response.start",
"status": self.status_code, "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: except Exception as e:
logger.exception(f"Error in stream_response: {str(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 def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
async with anyio.create_task_group() as task_group: 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: async def wrap(func: typing.Callable[[], typing.Awaitable[None]]) -> None:
try: try:
await func() 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: 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") logger.exception("Error in streaming task")
raise # Only re-raise if it's not a protocol error or cancellation
raise
finally: 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)) # Start the streaming response in a separate task
await wrap(partial(self.listen_for_disconnect, receive)) task_group.start_soon(wrap, stream_func)
# Listen for disconnect events
await wrap(listen_func)
if self.background is not None: if self.background is not None:
await self.background() await self.background()

View File

@@ -143,11 +143,13 @@ class M3U8Processor:
full_url = parse.urljoin(base_url, url) full_url = parse.urljoin(base_url, url)
query_params = dict(self.request.query_params) query_params = dict(self.request.query_params)
has_encrypted = query_params.pop("has_encrypted", False) 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( return encode_mediaflow_proxy_url(
self.mediaflow_proxy_url, self.mediaflow_proxy_url,
"", "",
full_url, full_url,
query_params=dict(self.request.query_params), query_params=query_params,
encryption_handler=encryption_handler if has_encrypted else None, encryption_handler=encryption_handler if has_encrypted else None,
) )