mirror of
https://github.com/UrloMythus/UnHided.git
synced 2026-04-09 02:40:47 +00:00
updated to newest version, dlhd support
This commit is contained in:
@@ -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:
|
||||
|
||||
335
mediaflow_proxy/extractors/dlhd.py
Normal file
335
mediaflow_proxy/extractors/dlhd.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)):
|
||||
|
||||
26
mediaflow_proxy/middleware.py
Normal file
26
mediaflow_proxy/middleware.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user