Spaces:
Running
Running
Refactor query parameter handling for proxy routes
Browse filesThis commit refactors the query parameter handling in the proxy routes by consolidating parameters into specific Pydantic models using the `Annotated` type for better clarity and maintainability. Additionally, extracted common setup and error-handling logic into reusable functions within `handlers.py`.
- mediaflow_proxy/handlers.py +148 -159
- mediaflow_proxy/routes.py +24 -65
- mediaflow_proxy/schemas.py +41 -1
mediaflow_proxy/handlers.py
CHANGED
@@ -9,6 +9,7 @@ from starlette.background import BackgroundTask
|
|
9 |
from .configs import settings
|
10 |
from .const import SUPPORTED_RESPONSE_HEADERS
|
11 |
from .mpd_processor import process_manifest, process_playlist, process_segment
|
|
|
12 |
from .utils.cache_utils import get_cached_mpd, get_cached_init_segment
|
13 |
from .utils.http_utils import (
|
14 |
Streamer,
|
@@ -24,27 +25,16 @@ from .utils.mpd_utils import pad_base64
|
|
24 |
logger = logging.getLogger(__name__)
|
25 |
|
26 |
|
27 |
-
async def
|
28 |
-
request: Request,
|
29 |
-
destination: str,
|
30 |
-
proxy_headers: ProxyRequestHeaders,
|
31 |
-
key_url: HttpUrl = None,
|
32 |
-
verify_ssl: bool = True,
|
33 |
-
use_request_proxy: bool = True,
|
34 |
-
):
|
35 |
"""
|
36 |
-
|
37 |
|
38 |
Args:
|
39 |
-
|
40 |
-
|
41 |
-
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
42 |
-
key_url (str, optional): The HLS Key URL to replace the original key URL. Defaults to None.
|
43 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
44 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
45 |
|
46 |
Returns:
|
47 |
-
|
48 |
"""
|
49 |
client = httpx.AsyncClient(
|
50 |
follow_redirects=True,
|
@@ -53,68 +43,72 @@ async def handle_hls_stream_proxy(
|
|
53 |
proxy=settings.proxy_url if use_request_proxy else None,
|
54 |
verify=verify_ssl,
|
55 |
)
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
try:
|
58 |
-
if destination.endswith((".m3u", ".m3u8")):
|
59 |
-
return await fetch_and_process_m3u8(
|
|
|
|
|
60 |
|
61 |
-
response = await streamer.head(destination, proxy_headers.request)
|
62 |
if "mpegurl" in response.headers.get("content-type", "").lower():
|
63 |
-
return await fetch_and_process_m3u8(
|
|
|
|
|
64 |
|
65 |
proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
|
66 |
-
|
67 |
-
response_headers = {k: v for k, v in response.headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
68 |
-
|
69 |
-
if transfer_encoding := response_headers.get("transfer-encoding"):
|
70 |
-
if "chunked" not in transfer_encoding:
|
71 |
-
transfer_encoding += ", chunked"
|
72 |
-
else:
|
73 |
-
transfer_encoding = "chunked"
|
74 |
-
response_headers["transfer-encoding"] = transfer_encoding
|
75 |
-
response_headers.update(proxy_headers.response)
|
76 |
|
77 |
return EnhancedStreamingResponse(
|
78 |
-
streamer.stream_content(destination, proxy_headers.request),
|
79 |
status_code=response.status_code,
|
80 |
headers=response_headers,
|
81 |
background=BackgroundTask(streamer.close),
|
82 |
)
|
83 |
-
except httpx.HTTPStatusError as e:
|
84 |
-
await client.aclose()
|
85 |
-
logger.error(f"Upstream service error while handling request: {e}")
|
86 |
-
return Response(status_code=e.response.status_code, content=f"Upstream service error: {e}")
|
87 |
-
except DownloadError as e:
|
88 |
-
await client.aclose()
|
89 |
-
logger.error(f"Error downloading {destination}: {e}")
|
90 |
-
return Response(status_code=e.status_code, content=str(e))
|
91 |
except Exception as e:
|
92 |
await client.aclose()
|
93 |
-
|
94 |
-
return Response(status_code=502, content=f"Internal server error: {e}")
|
95 |
-
|
96 |
-
|
97 |
-
async def proxy_stream(
|
98 |
-
method: str,
|
99 |
-
video_url: str,
|
100 |
-
proxy_headers: ProxyRequestHeaders,
|
101 |
-
verify_ssl: bool = True,
|
102 |
-
use_request_proxy: bool = True,
|
103 |
-
):
|
104 |
-
"""
|
105 |
-
Proxies the stream request to the given video URL.
|
106 |
-
|
107 |
-
Args:
|
108 |
-
method (str): The HTTP method (e.g., GET, HEAD).
|
109 |
-
video_url (str): The URL of the video to stream.
|
110 |
-
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
111 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
112 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
113 |
-
|
114 |
-
Returns:
|
115 |
-
Response: The HTTP response with the streamed content.
|
116 |
-
"""
|
117 |
-
return await handle_stream_request(method, video_url, proxy_headers, verify_ssl, use_request_proxy)
|
118 |
|
119 |
|
120 |
async def handle_stream_request(
|
@@ -123,39 +117,27 @@ async def handle_stream_request(
|
|
123 |
proxy_headers: ProxyRequestHeaders,
|
124 |
verify_ssl: bool = True,
|
125 |
use_request_proxy: bool = True,
|
126 |
-
):
|
127 |
"""
|
128 |
-
|
|
|
|
|
129 |
|
130 |
Args:
|
131 |
-
method (str): The HTTP method (e.g., GET
|
132 |
video_url (str): The URL of the video to stream.
|
133 |
-
proxy_headers (ProxyRequestHeaders):
|
134 |
-
verify_ssl (bool, optional): Whether to verify
|
135 |
-
use_request_proxy (bool, optional): Whether to use
|
136 |
|
137 |
Returns:
|
138 |
-
Response:
|
139 |
"""
|
140 |
-
client =
|
141 |
-
|
142 |
-
timeout=httpx.Timeout(30.0),
|
143 |
-
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
144 |
-
proxy=settings.proxy_url if use_request_proxy else None,
|
145 |
-
verify=verify_ssl,
|
146 |
-
)
|
147 |
-
streamer = Streamer(client)
|
148 |
try:
|
149 |
response = await streamer.head(video_url, proxy_headers.request)
|
150 |
-
|
151 |
-
response_headers = {k: v for k, v in response.headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
152 |
-
if transfer_encoding := response_headers.get("transfer-encoding"):
|
153 |
-
if "chunked" not in transfer_encoding:
|
154 |
-
transfer_encoding += ", chunked"
|
155 |
-
else:
|
156 |
-
transfer_encoding = "chunked"
|
157 |
-
response_headers["transfer-encoding"] = transfer_encoding
|
158 |
-
response_headers.update(proxy_headers.response)
|
159 |
|
160 |
if method == "HEAD":
|
161 |
await streamer.close()
|
@@ -167,18 +149,49 @@ async def handle_stream_request(
|
|
167 |
status_code=response.status_code,
|
168 |
background=BackgroundTask(streamer.close),
|
169 |
)
|
170 |
-
except httpx.HTTPStatusError as e:
|
171 |
-
await client.aclose()
|
172 |
-
logger.error(f"Upstream service error while handling {method} request: {e}")
|
173 |
-
return Response(status_code=e.response.status_code, content=f"Upstream service error: {e}")
|
174 |
-
except DownloadError as e:
|
175 |
-
await client.aclose()
|
176 |
-
logger.error(f"Error downloading {video_url}: {e}")
|
177 |
-
return Response(status_code=e.status_code, content=str(e))
|
178 |
except Exception as e:
|
179 |
await client.aclose()
|
180 |
-
|
181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
|
183 |
|
184 |
async def fetch_and_process_m3u8(
|
@@ -208,15 +221,8 @@ async def fetch_and_process_m3u8(
|
|
208 |
media_type="application/vnd.apple.mpegurl",
|
209 |
headers=response_headers,
|
210 |
)
|
211 |
-
except httpx.HTTPStatusError as e:
|
212 |
-
logger.error(f"HTTP error while fetching m3u8: {e}")
|
213 |
-
return Response(status_code=e.response.status_code, content=str(e))
|
214 |
-
except DownloadError as e:
|
215 |
-
logger.error(f"Error downloading m3u8: {url}")
|
216 |
-
return Response(status_code=502, content=str(e))
|
217 |
except Exception as e:
|
218 |
-
|
219 |
-
return Response(status_code=502, content=str(e))
|
220 |
finally:
|
221 |
await streamer.close()
|
222 |
|
@@ -252,35 +258,27 @@ async def handle_drm_key_data(key_id, key, drm_info):
|
|
252 |
|
253 |
async def get_manifest(
|
254 |
request: Request,
|
255 |
-
|
256 |
proxy_headers: ProxyRequestHeaders,
|
257 |
-
key_id: str = None,
|
258 |
-
key: str = None,
|
259 |
-
verify_ssl: bool = True,
|
260 |
-
use_request_proxy: bool = True,
|
261 |
):
|
262 |
"""
|
263 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
264 |
|
265 |
Args:
|
266 |
request (Request): The incoming HTTP request.
|
267 |
-
|
268 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
269 |
-
key_id (str, optional): The DRM key ID. Defaults to None.
|
270 |
-
key (str, optional): The DRM key. Defaults to None.
|
271 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
272 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
273 |
|
274 |
Returns:
|
275 |
Response: The HTTP response with the HLS manifest.
|
276 |
"""
|
277 |
try:
|
278 |
mpd_dict = await get_cached_mpd(
|
279 |
-
|
280 |
headers=proxy_headers.request,
|
281 |
-
parse_drm=not key_id and not key,
|
282 |
-
verify_ssl=verify_ssl,
|
283 |
-
use_request_proxy=use_request_proxy,
|
284 |
)
|
285 |
except DownloadError as e:
|
286 |
raise HTTPException(status_code=e.status_code, detail=f"Failed to download MPD: {e.message}")
|
@@ -290,7 +288,7 @@ async def get_manifest(
|
|
290 |
# For non-DRM protected MPD, we still create an HLS manifest
|
291 |
return await process_manifest(request, mpd_dict, proxy_headers, None, None)
|
292 |
|
293 |
-
key_id, key = await handle_drm_key_data(key_id, key, drm_info)
|
294 |
|
295 |
# check if the provided key_id and key are valid
|
296 |
if key_id and len(key_id) != 32:
|
@@ -303,75 +301,66 @@ async def get_manifest(
|
|
303 |
|
304 |
async def get_playlist(
|
305 |
request: Request,
|
306 |
-
|
307 |
-
profile_id: str,
|
308 |
proxy_headers: ProxyRequestHeaders,
|
309 |
-
key_id: str = None,
|
310 |
-
key: str = None,
|
311 |
-
verify_ssl: bool = True,
|
312 |
-
use_request_proxy: bool = True,
|
313 |
):
|
314 |
"""
|
315 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
316 |
|
317 |
Args:
|
318 |
request (Request): The incoming HTTP request.
|
319 |
-
|
320 |
-
profile_id (str): The profile ID to generate the playlist for.
|
321 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
322 |
-
key_id (str, optional): The DRM key ID. Defaults to None.
|
323 |
-
key (str, optional): The DRM key. Defaults to None.
|
324 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
325 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
326 |
|
327 |
Returns:
|
328 |
Response: The HTTP response with the HLS playlist.
|
329 |
"""
|
330 |
mpd_dict = await get_cached_mpd(
|
331 |
-
|
332 |
headers=proxy_headers.request,
|
333 |
-
parse_drm=not key_id and not key,
|
334 |
-
parse_segment_profile_id=profile_id,
|
335 |
-
verify_ssl=verify_ssl,
|
336 |
-
use_request_proxy=use_request_proxy,
|
337 |
)
|
338 |
-
return await process_playlist(request, mpd_dict, profile_id, proxy_headers)
|
339 |
|
340 |
|
341 |
async def get_segment(
|
342 |
-
|
343 |
-
segment_url: str,
|
344 |
-
mimetype: str,
|
345 |
proxy_headers: ProxyRequestHeaders,
|
346 |
-
key_id: str = None,
|
347 |
-
key: str = None,
|
348 |
-
verify_ssl: bool = True,
|
349 |
-
use_request_proxy: bool = True,
|
350 |
):
|
351 |
"""
|
352 |
Retrieves and processes a media segment, decrypting it if necessary.
|
353 |
|
354 |
Args:
|
355 |
-
|
356 |
-
segment_url (str): The URL of the media segment.
|
357 |
-
mimetype (str): The MIME type of the segment.
|
358 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
359 |
-
key_id (str, optional): The DRM key ID. Defaults to None.
|
360 |
-
key (str, optional): The DRM key. Defaults to None.
|
361 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
|
362 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
363 |
|
364 |
Returns:
|
365 |
Response: The HTTP response with the processed segment.
|
366 |
"""
|
367 |
try:
|
368 |
-
init_content = await get_cached_init_segment(
|
|
|
|
|
369 |
segment_content = await download_file_with_retry(
|
370 |
-
segment_url,
|
|
|
|
|
|
|
371 |
)
|
372 |
-
except
|
373 |
-
|
374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
375 |
|
376 |
|
377 |
async def get_public_ip(use_request_proxy: bool = True):
|
|
|
9 |
from .configs import settings
|
10 |
from .const import SUPPORTED_RESPONSE_HEADERS
|
11 |
from .mpd_processor import process_manifest, process_playlist, process_segment
|
12 |
+
from .schemas import HLSManifestParams, ProxyStreamParams, MPDManifestParams, MPDPlaylistParams, MPDSegmentParams
|
13 |
from .utils.cache_utils import get_cached_mpd, get_cached_init_segment
|
14 |
from .utils.http_utils import (
|
15 |
Streamer,
|
|
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
|
28 |
+
async def setup_client_and_streamer(use_request_proxy: bool, verify_ssl: bool) -> tuple[httpx.AsyncClient, Streamer]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
"""
|
30 |
+
Set up an HTTP client and a streamer.
|
31 |
|
32 |
Args:
|
33 |
+
use_request_proxy (bool): Whether to use a proxy for the request.
|
34 |
+
verify_ssl (bool): Whether to verify SSL certificates.
|
|
|
|
|
|
|
|
|
35 |
|
36 |
Returns:
|
37 |
+
tuple: An httpx.AsyncClient instance and a Streamer instance.
|
38 |
"""
|
39 |
client = httpx.AsyncClient(
|
40 |
follow_redirects=True,
|
|
|
43 |
proxy=settings.proxy_url if use_request_proxy else None,
|
44 |
verify=verify_ssl,
|
45 |
)
|
46 |
+
return client, Streamer(client)
|
47 |
+
|
48 |
+
|
49 |
+
def handle_exceptions(exception: Exception) -> Response:
|
50 |
+
"""
|
51 |
+
Handle exceptions and return appropriate HTTP responses.
|
52 |
+
|
53 |
+
Args:
|
54 |
+
exception (Exception): The exception that was raised.
|
55 |
+
|
56 |
+
Returns:
|
57 |
+
Response: An HTTP response corresponding to the exception type.
|
58 |
+
"""
|
59 |
+
if isinstance(exception, httpx.HTTPStatusError):
|
60 |
+
logger.error(f"Upstream service error while handling request: {exception}")
|
61 |
+
return Response(status_code=exception.response.status_code, content=f"Upstream service error: {exception}")
|
62 |
+
elif isinstance(exception, DownloadError):
|
63 |
+
logger.error(f"Error downloading content: {exception}")
|
64 |
+
return Response(status_code=exception.status_code, content=str(exception))
|
65 |
+
else:
|
66 |
+
logger.error(f"Internal server error while handling request: {exception}")
|
67 |
+
return Response(status_code=502, content=f"Internal server error: {exception}")
|
68 |
+
|
69 |
+
|
70 |
+
async def handle_hls_stream_proxy(
|
71 |
+
request: Request, hls_params: HLSManifestParams, proxy_headers: ProxyRequestHeaders
|
72 |
+
) -> Response:
|
73 |
+
"""
|
74 |
+
Handle HLS stream proxy requests.
|
75 |
+
|
76 |
+
This function processes HLS manifest files and streams content based on the request parameters.
|
77 |
+
|
78 |
+
Args:
|
79 |
+
request (Request): The incoming FastAPI request object.
|
80 |
+
hls_params (HLSManifestParams): Parameters for the HLS manifest.
|
81 |
+
proxy_headers (ProxyRequestHeaders): Headers to be used in the proxy request.
|
82 |
+
|
83 |
+
Returns:
|
84 |
+
Union[Response, EnhancedStreamingResponse]: Either a processed m3u8 playlist or a streaming response.
|
85 |
+
"""
|
86 |
+
client, streamer = await setup_client_and_streamer(hls_params.use_request_proxy, hls_params.verify_ssl)
|
87 |
+
|
88 |
try:
|
89 |
+
if hls_params.destination.endswith((".m3u", ".m3u8")):
|
90 |
+
return await fetch_and_process_m3u8(
|
91 |
+
streamer, hls_params.destination, proxy_headers, request, hls_params.key_url
|
92 |
+
)
|
93 |
|
94 |
+
response = await streamer.head(hls_params.destination, proxy_headers.request)
|
95 |
if "mpegurl" in response.headers.get("content-type", "").lower():
|
96 |
+
return await fetch_and_process_m3u8(
|
97 |
+
streamer, hls_params.destination, proxy_headers, request, hls_params.key_url
|
98 |
+
)
|
99 |
|
100 |
proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
|
101 |
+
response_headers = prepare_response_headers(response.headers, proxy_headers.response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
|
103 |
return EnhancedStreamingResponse(
|
104 |
+
streamer.stream_content(hls_params.destination, proxy_headers.request),
|
105 |
status_code=response.status_code,
|
106 |
headers=response_headers,
|
107 |
background=BackgroundTask(streamer.close),
|
108 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
except Exception as e:
|
110 |
await client.aclose()
|
111 |
+
return handle_exceptions(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
|
114 |
async def handle_stream_request(
|
|
|
117 |
proxy_headers: ProxyRequestHeaders,
|
118 |
verify_ssl: bool = True,
|
119 |
use_request_proxy: bool = True,
|
120 |
+
) -> Response:
|
121 |
"""
|
122 |
+
Handle general stream requests.
|
123 |
+
|
124 |
+
This function processes both HEAD and GET requests for video streams.
|
125 |
|
126 |
Args:
|
127 |
+
method (str): The HTTP method (e.g., 'GET' or 'HEAD').
|
128 |
video_url (str): The URL of the video to stream.
|
129 |
+
proxy_headers (ProxyRequestHeaders): Headers to be used in the proxy request.
|
130 |
+
verify_ssl (bool, optional): Whether to verify SSL certificates. Defaults to True.
|
131 |
+
use_request_proxy (bool, optional): Whether to use a proxy for the request. Defaults to True.
|
132 |
|
133 |
Returns:
|
134 |
+
Union[Response, EnhancedStreamingResponse]: Either a HEAD response or a streaming response.
|
135 |
"""
|
136 |
+
client, streamer = await setup_client_and_streamer(use_request_proxy, verify_ssl)
|
137 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
try:
|
139 |
response = await streamer.head(video_url, proxy_headers.request)
|
140 |
+
response_headers = prepare_response_headers(response.headers, proxy_headers.response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
|
142 |
if method == "HEAD":
|
143 |
await streamer.close()
|
|
|
149 |
status_code=response.status_code,
|
150 |
background=BackgroundTask(streamer.close),
|
151 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
except Exception as e:
|
153 |
await client.aclose()
|
154 |
+
return handle_exceptions(e)
|
155 |
+
|
156 |
+
|
157 |
+
def prepare_response_headers(original_headers, proxy_response_headers) -> dict:
|
158 |
+
"""
|
159 |
+
Prepare response headers for the proxy response.
|
160 |
+
|
161 |
+
This function filters the original headers, ensures proper transfer encoding,
|
162 |
+
and merges them with the proxy response headers.
|
163 |
+
|
164 |
+
Args:
|
165 |
+
original_headers (httpx.Headers): The original headers from the upstream response.
|
166 |
+
proxy_response_headers (dict): Additional headers to be included in the proxy response.
|
167 |
+
|
168 |
+
Returns:
|
169 |
+
dict: The prepared headers for the proxy response.
|
170 |
+
"""
|
171 |
+
response_headers = {k: v for k, v in original_headers.multi_items() if k in SUPPORTED_RESPONSE_HEADERS}
|
172 |
+
transfer_encoding = response_headers.get("transfer-encoding", "")
|
173 |
+
if "chunked" not in transfer_encoding:
|
174 |
+
transfer_encoding += ", chunked" if transfer_encoding else "chunked"
|
175 |
+
response_headers["transfer-encoding"] = transfer_encoding
|
176 |
+
response_headers.update(proxy_response_headers)
|
177 |
+
return response_headers
|
178 |
+
|
179 |
+
|
180 |
+
async def proxy_stream(method: str, stream_params: ProxyStreamParams, proxy_headers: ProxyRequestHeaders):
|
181 |
+
"""
|
182 |
+
Proxies the stream request to the given video URL.
|
183 |
+
|
184 |
+
Args:
|
185 |
+
method (str): The HTTP method (e.g., GET, HEAD).
|
186 |
+
stream_params (ProxyStreamParams): The parameters for the stream request.
|
187 |
+
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
188 |
+
|
189 |
+
Returns:
|
190 |
+
Response: The HTTP response with the streamed content.
|
191 |
+
"""
|
192 |
+
return await handle_stream_request(
|
193 |
+
method, stream_params.destination, proxy_headers, stream_params.verify_ssl, stream_params.use_request_proxy
|
194 |
+
)
|
195 |
|
196 |
|
197 |
async def fetch_and_process_m3u8(
|
|
|
221 |
media_type="application/vnd.apple.mpegurl",
|
222 |
headers=response_headers,
|
223 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
except Exception as e:
|
225 |
+
return handle_exceptions(e)
|
|
|
226 |
finally:
|
227 |
await streamer.close()
|
228 |
|
|
|
258 |
|
259 |
async def get_manifest(
|
260 |
request: Request,
|
261 |
+
manifest_params: MPDManifestParams,
|
262 |
proxy_headers: ProxyRequestHeaders,
|
|
|
|
|
|
|
|
|
263 |
):
|
264 |
"""
|
265 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
266 |
|
267 |
Args:
|
268 |
request (Request): The incoming HTTP request.
|
269 |
+
manifest_params (MPDManifestParams): The parameters for the manifest request.
|
270 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
|
|
|
|
271 |
|
272 |
Returns:
|
273 |
Response: The HTTP response with the HLS manifest.
|
274 |
"""
|
275 |
try:
|
276 |
mpd_dict = await get_cached_mpd(
|
277 |
+
manifest_params.destination,
|
278 |
headers=proxy_headers.request,
|
279 |
+
parse_drm=not manifest_params.key_id and not manifest_params.key,
|
280 |
+
verify_ssl=manifest_params.verify_ssl,
|
281 |
+
use_request_proxy=manifest_params.use_request_proxy,
|
282 |
)
|
283 |
except DownloadError as e:
|
284 |
raise HTTPException(status_code=e.status_code, detail=f"Failed to download MPD: {e.message}")
|
|
|
288 |
# For non-DRM protected MPD, we still create an HLS manifest
|
289 |
return await process_manifest(request, mpd_dict, proxy_headers, None, None)
|
290 |
|
291 |
+
key_id, key = await handle_drm_key_data(manifest_params.key_id, manifest_params.key, drm_info)
|
292 |
|
293 |
# check if the provided key_id and key are valid
|
294 |
if key_id and len(key_id) != 32:
|
|
|
301 |
|
302 |
async def get_playlist(
|
303 |
request: Request,
|
304 |
+
playlist_params: MPDPlaylistParams,
|
|
|
305 |
proxy_headers: ProxyRequestHeaders,
|
|
|
|
|
|
|
|
|
306 |
):
|
307 |
"""
|
308 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
309 |
|
310 |
Args:
|
311 |
request (Request): The incoming HTTP request.
|
312 |
+
playlist_params (MPDPlaylistParams): The parameters for the playlist request.
|
|
|
313 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
|
|
|
|
314 |
|
315 |
Returns:
|
316 |
Response: The HTTP response with the HLS playlist.
|
317 |
"""
|
318 |
mpd_dict = await get_cached_mpd(
|
319 |
+
playlist_params.destination,
|
320 |
headers=proxy_headers.request,
|
321 |
+
parse_drm=not playlist_params.key_id and not playlist_params.key,
|
322 |
+
parse_segment_profile_id=playlist_params.profile_id,
|
323 |
+
verify_ssl=playlist_params.verify_ssl,
|
324 |
+
use_request_proxy=playlist_params.use_request_proxy,
|
325 |
)
|
326 |
+
return await process_playlist(request, mpd_dict, playlist_params.profile_id, proxy_headers)
|
327 |
|
328 |
|
329 |
async def get_segment(
|
330 |
+
segment_params: MPDSegmentParams,
|
|
|
|
|
331 |
proxy_headers: ProxyRequestHeaders,
|
|
|
|
|
|
|
|
|
332 |
):
|
333 |
"""
|
334 |
Retrieves and processes a media segment, decrypting it if necessary.
|
335 |
|
336 |
Args:
|
337 |
+
segment_params (MPDSegmentParams): The parameters for the segment request.
|
|
|
|
|
338 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
|
|
|
|
339 |
|
340 |
Returns:
|
341 |
Response: The HTTP response with the processed segment.
|
342 |
"""
|
343 |
try:
|
344 |
+
init_content = await get_cached_init_segment(
|
345 |
+
segment_params.init_url, proxy_headers.request, segment_params.verify_ssl, segment_params.use_request_proxy
|
346 |
+
)
|
347 |
segment_content = await download_file_with_retry(
|
348 |
+
segment_params.segment_url,
|
349 |
+
proxy_headers.request,
|
350 |
+
verify_ssl=segment_params.verify_ssl,
|
351 |
+
use_request_proxy=segment_params.use_request_proxy,
|
352 |
)
|
353 |
+
except Exception as e:
|
354 |
+
return handle_exceptions(e)
|
355 |
+
|
356 |
+
return await process_segment(
|
357 |
+
init_content,
|
358 |
+
segment_content,
|
359 |
+
segment_params.mime_type,
|
360 |
+
proxy_headers,
|
361 |
+
segment_params.key_id,
|
362 |
+
segment_params.key,
|
363 |
+
)
|
364 |
|
365 |
|
366 |
async def get_public_ip(use_request_proxy: bool = True):
|
mediaflow_proxy/routes.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1 |
-
from
|
2 |
-
|
|
|
3 |
|
4 |
from .handlers import handle_hls_stream_proxy, proxy_stream, get_manifest, get_playlist, get_segment, get_public_ip
|
|
|
5 |
from .utils.http_utils import get_proxy_headers, ProxyRequestHeaders
|
6 |
|
7 |
proxy_router = APIRouter()
|
@@ -11,144 +13,101 @@ proxy_router = APIRouter()
|
|
11 |
@proxy_router.get("/hls/manifest.m3u8")
|
12 |
async def hls_stream_proxy(
|
13 |
request: Request,
|
14 |
-
|
15 |
-
proxy_headers: ProxyRequestHeaders
|
16 |
-
key_url: HttpUrl | None = None,
|
17 |
-
verify_ssl: bool = False,
|
18 |
-
use_request_proxy: bool = True,
|
19 |
):
|
20 |
"""
|
21 |
Proxify HLS stream requests, fetching and processing the m3u8 playlist or streaming the content.
|
22 |
|
23 |
Args:
|
24 |
request (Request): The incoming HTTP request.
|
25 |
-
|
26 |
-
key_url (HttpUrl, optional): The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)
|
27 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
28 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
29 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
30 |
|
31 |
Returns:
|
32 |
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
33 |
"""
|
34 |
-
|
35 |
-
return await handle_hls_stream_proxy(request, destination, proxy_headers, key_url, verify_ssl, use_request_proxy)
|
36 |
|
37 |
|
38 |
@proxy_router.head("/stream")
|
39 |
@proxy_router.get("/stream")
|
40 |
async def proxy_stream_endpoint(
|
41 |
request: Request,
|
42 |
-
|
43 |
-
proxy_headers: ProxyRequestHeaders
|
44 |
-
verify_ssl: bool = False,
|
45 |
-
use_request_proxy: bool = True,
|
46 |
):
|
47 |
"""
|
48 |
Proxies stream requests to the given video URL.
|
49 |
|
50 |
Args:
|
51 |
request (Request): The incoming HTTP request.
|
52 |
-
|
53 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
54 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
55 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
56 |
|
57 |
Returns:
|
58 |
Response: The HTTP response with the streamed content.
|
59 |
"""
|
60 |
proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
|
61 |
-
return await proxy_stream(request.method,
|
62 |
|
63 |
|
64 |
@proxy_router.get("/mpd/manifest.m3u8")
|
65 |
async def manifest_endpoint(
|
66 |
request: Request,
|
67 |
-
|
68 |
-
proxy_headers: ProxyRequestHeaders
|
69 |
-
key_id: str = None,
|
70 |
-
key: str = None,
|
71 |
-
verify_ssl: bool = False,
|
72 |
-
use_request_proxy: bool = True,
|
73 |
):
|
74 |
"""
|
75 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
76 |
|
77 |
Args:
|
78 |
request (Request): The incoming HTTP request.
|
79 |
-
|
80 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
81 |
-
key_id (str, optional): The DRM key ID. Defaults to None.
|
82 |
-
key (str, optional): The DRM key. Defaults to None.
|
83 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
84 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
85 |
|
86 |
Returns:
|
87 |
Response: The HTTP response with the HLS manifest.
|
88 |
"""
|
89 |
-
return await get_manifest(request,
|
90 |
|
91 |
|
92 |
@proxy_router.get("/mpd/playlist.m3u8")
|
93 |
async def playlist_endpoint(
|
94 |
request: Request,
|
95 |
-
|
96 |
-
|
97 |
-
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
98 |
-
key_id: str = None,
|
99 |
-
key: str = None,
|
100 |
-
verify_ssl: bool = False,
|
101 |
-
use_request_proxy: bool = True,
|
102 |
):
|
103 |
"""
|
104 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
105 |
|
106 |
Args:
|
107 |
request (Request): The incoming HTTP request.
|
108 |
-
|
109 |
-
profile_id (str): The profile ID to generate the playlist for.
|
110 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
111 |
-
key_id (str, optional): The DRM key ID. Defaults to None.
|
112 |
-
key (str, optional): The DRM key. Defaults to None.
|
113 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
114 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
115 |
|
116 |
Returns:
|
117 |
Response: The HTTP response with the HLS playlist.
|
118 |
"""
|
119 |
-
return await get_playlist(request,
|
120 |
|
121 |
|
122 |
@proxy_router.get("/mpd/segment")
|
123 |
async def segment_endpoint(
|
124 |
-
|
125 |
-
|
126 |
-
mime_type: str,
|
127 |
-
proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
|
128 |
-
key_id: str = None,
|
129 |
-
key: str = None,
|
130 |
-
verify_ssl: bool = False,
|
131 |
-
use_request_proxy: bool = True,
|
132 |
):
|
133 |
"""
|
134 |
Retrieves and processes a media segment, decrypting it if necessary.
|
135 |
|
136 |
Args:
|
137 |
-
|
138 |
-
segment_url (HttpUrl): The URL of the media segment.
|
139 |
-
mime_type (str): The MIME type of the segment.
|
140 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
141 |
-
key_id (str, optional): The DRM key ID. Defaults to None.
|
142 |
-
key (str, optional): The DRM key. Defaults to None.
|
143 |
-
verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to False.
|
144 |
-
use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
|
145 |
|
146 |
Returns:
|
147 |
Response: The HTTP response with the processed segment.
|
148 |
"""
|
149 |
-
return await get_segment(
|
150 |
-
str(init_url), str(segment_url), mime_type, proxy_headers, key_id, key, verify_ssl, use_request_proxy
|
151 |
-
)
|
152 |
|
153 |
|
154 |
@proxy_router.get("/ip")
|
|
|
1 |
+
from typing import Annotated
|
2 |
+
|
3 |
+
from fastapi import Request, Depends, APIRouter, Query
|
4 |
|
5 |
from .handlers import handle_hls_stream_proxy, proxy_stream, get_manifest, get_playlist, get_segment, get_public_ip
|
6 |
+
from .schemas import MPDSegmentParams, MPDPlaylistParams, HLSManifestParams, ProxyStreamParams, MPDManifestParams
|
7 |
from .utils.http_utils import get_proxy_headers, ProxyRequestHeaders
|
8 |
|
9 |
proxy_router = APIRouter()
|
|
|
13 |
@proxy_router.get("/hls/manifest.m3u8")
|
14 |
async def hls_stream_proxy(
|
15 |
request: Request,
|
16 |
+
hls_params: Annotated[HLSManifestParams, Query()],
|
17 |
+
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
|
|
|
|
|
|
18 |
):
|
19 |
"""
|
20 |
Proxify HLS stream requests, fetching and processing the m3u8 playlist or streaming the content.
|
21 |
|
22 |
Args:
|
23 |
request (Request): The incoming HTTP request.
|
24 |
+
hls_params (HLSPlaylistParams): The parameters for the HLS stream request.
|
|
|
25 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
26 |
|
27 |
Returns:
|
28 |
Response: The HTTP response with the processed m3u8 playlist or streamed content.
|
29 |
"""
|
30 |
+
return await handle_hls_stream_proxy(request, hls_params, proxy_headers)
|
|
|
31 |
|
32 |
|
33 |
@proxy_router.head("/stream")
|
34 |
@proxy_router.get("/stream")
|
35 |
async def proxy_stream_endpoint(
|
36 |
request: Request,
|
37 |
+
stream_params: Annotated[ProxyStreamParams, Query()],
|
38 |
+
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
|
|
|
|
39 |
):
|
40 |
"""
|
41 |
Proxies stream requests to the given video URL.
|
42 |
|
43 |
Args:
|
44 |
request (Request): The incoming HTTP request.
|
45 |
+
stream_params (ProxyStreamParams): The parameters for the stream request.
|
46 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
47 |
|
48 |
Returns:
|
49 |
Response: The HTTP response with the streamed content.
|
50 |
"""
|
51 |
proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
|
52 |
+
return await proxy_stream(request.method, stream_params, proxy_headers)
|
53 |
|
54 |
|
55 |
@proxy_router.get("/mpd/manifest.m3u8")
|
56 |
async def manifest_endpoint(
|
57 |
request: Request,
|
58 |
+
manifest_params: Annotated[MPDManifestParams, Query()],
|
59 |
+
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
|
|
|
|
|
|
|
|
60 |
):
|
61 |
"""
|
62 |
Retrieves and processes the MPD manifest, converting it to an HLS manifest.
|
63 |
|
64 |
Args:
|
65 |
request (Request): The incoming HTTP request.
|
66 |
+
manifest_params (MPDManifestParams): The parameters for the manifest request.
|
67 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
|
|
|
|
68 |
|
69 |
Returns:
|
70 |
Response: The HTTP response with the HLS manifest.
|
71 |
"""
|
72 |
+
return await get_manifest(request, manifest_params, proxy_headers)
|
73 |
|
74 |
|
75 |
@proxy_router.get("/mpd/playlist.m3u8")
|
76 |
async def playlist_endpoint(
|
77 |
request: Request,
|
78 |
+
playlist_params: Annotated[MPDPlaylistParams, Query()],
|
79 |
+
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
|
|
|
|
|
|
|
|
|
|
80 |
):
|
81 |
"""
|
82 |
Retrieves and processes the MPD manifest, converting it to an HLS playlist for a specific profile.
|
83 |
|
84 |
Args:
|
85 |
request (Request): The incoming HTTP request.
|
86 |
+
playlist_params (MPDPlaylistParams): The parameters for the playlist request.
|
|
|
87 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
|
|
|
|
88 |
|
89 |
Returns:
|
90 |
Response: The HTTP response with the HLS playlist.
|
91 |
"""
|
92 |
+
return await get_playlist(request, playlist_params, proxy_headers)
|
93 |
|
94 |
|
95 |
@proxy_router.get("/mpd/segment")
|
96 |
async def segment_endpoint(
|
97 |
+
segment_params: Annotated[MPDSegmentParams, Query()],
|
98 |
+
proxy_headers: Annotated[ProxyRequestHeaders, Depends(get_proxy_headers)],
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
):
|
100 |
"""
|
101 |
Retrieves and processes a media segment, decrypting it if necessary.
|
102 |
|
103 |
Args:
|
104 |
+
segment_params (MPDSegmentParams): The parameters for the segment request.
|
|
|
|
|
105 |
proxy_headers (ProxyRequestHeaders): The headers to include in the request.
|
|
|
|
|
|
|
|
|
106 |
|
107 |
Returns:
|
108 |
Response: The HTTP response with the processed segment.
|
109 |
"""
|
110 |
+
return await get_segment(segment_params, proxy_headers)
|
|
|
|
|
111 |
|
112 |
|
113 |
@proxy_router.get("/ip")
|
mediaflow_proxy/schemas.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
from pydantic import BaseModel, Field, IPvAnyAddress
|
2 |
|
3 |
|
4 |
class GenerateUrlRequest(BaseModel):
|
@@ -15,3 +15,43 @@ class GenerateUrlRequest(BaseModel):
|
|
15 |
None, description="API password for encryption. If not provided, the URL will only be encoded."
|
16 |
)
|
17 |
ip: IPvAnyAddress | None = Field(None, description="The IP address to restrict the URL to.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field, IPvAnyAddress, ConfigDict
|
2 |
|
3 |
|
4 |
class GenerateUrlRequest(BaseModel):
|
|
|
15 |
None, description="API password for encryption. If not provided, the URL will only be encoded."
|
16 |
)
|
17 |
ip: IPvAnyAddress | None = Field(None, description="The IP address to restrict the URL to.")
|
18 |
+
|
19 |
+
|
20 |
+
class GenericParams(BaseModel):
|
21 |
+
model_config = ConfigDict(populate_by_name=True)
|
22 |
+
|
23 |
+
verify_ssl: bool = Field(False, description="Whether to verify the SSL certificate of the destination.")
|
24 |
+
use_request_proxy: bool = Field(True, description="Whether to use the MediaFlow proxy configuration.")
|
25 |
+
|
26 |
+
|
27 |
+
class HLSManifestParams(GenericParams):
|
28 |
+
destination: str = Field(..., description="The URL of the HLS manifest.", alias="d")
|
29 |
+
key_url: str | None = Field(
|
30 |
+
None,
|
31 |
+
description="The HLS Key URL to replace the original key URL. Defaults to None. (Useful for bypassing some sneaky protection)",
|
32 |
+
)
|
33 |
+
|
34 |
+
|
35 |
+
class ProxyStreamParams(GenericParams):
|
36 |
+
destination: str = Field(..., description="The URL of the stream.", alias="d")
|
37 |
+
|
38 |
+
|
39 |
+
class MPDManifestParams(GenericParams):
|
40 |
+
destination: str = Field(..., description="The URL of the MPD manifest.", alias="d")
|
41 |
+
key_id: str | None = Field(None, description="The DRM key ID (optional).")
|
42 |
+
key: str | None = Field(None, description="The DRM key (optional).")
|
43 |
+
|
44 |
+
|
45 |
+
class MPDPlaylistParams(GenericParams):
|
46 |
+
destination: str = Field(..., description="The URL of the MPD manifest.", alias="d")
|
47 |
+
profile_id: str = Field(..., description="The profile ID to generate the playlist for.")
|
48 |
+
key_id: str | None = Field(None, description="The DRM key ID (optional).")
|
49 |
+
key: str | None = Field(None, description="The DRM key (optional).")
|
50 |
+
|
51 |
+
|
52 |
+
class MPDSegmentParams(GenericParams):
|
53 |
+
init_url: str = Field(..., description="The URL of the initialization segment.")
|
54 |
+
segment_url: str = Field(..., description="The URL of the media segment.")
|
55 |
+
mime_type: str = Field(..., description="The MIME type of the segment.")
|
56 |
+
key_id: str | None = Field(None, description="The DRM key ID (optional).")
|
57 |
+
key: str | None = Field(None, description="The DRM key (optional).")
|