mhdzumair commited on
Commit
8e6ac63
·
1 Parent(s): 0d394b4

Refactor query parameter handling for proxy routes

Browse files

This 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 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 handle_hls_stream_proxy(
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
- Handles the HLS stream proxy request, fetching and processing the m3u8 playlist or streaming the content.
37
 
38
  Args:
39
- request (Request): The incoming HTTP request.
40
- destination (str): The destination URL to fetch the content from.
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
- Response: The HTTP response with the processed m3u8 playlist or streamed content.
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
- streamer = Streamer(client)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  try:
58
- if destination.endswith((".m3u", ".m3u8")):
59
- return await fetch_and_process_m3u8(streamer, destination, proxy_headers, request, key_url)
 
 
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(streamer, destination, proxy_headers, request, key_url)
 
 
64
 
65
  proxy_headers.request.update({"range": proxy_headers.request.get("range", "bytes=0-")})
66
- # clean up the headers to only include the necessary headers and remove acl headers
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
- logger.error(f"Internal server error while handling request: {e}")
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
- Handles the stream request, fetching the content from the video URL and streaming it.
 
 
129
 
130
  Args:
131
- method (str): The HTTP method (e.g., GET, HEAD).
132
  video_url (str): The URL of the video to stream.
133
- proxy_headers (ProxyRequestHeaders): The headers to include in the request.
134
- verify_ssl (bool, optional): Whether to verify the SSL certificate of the destination. Defaults to True.
135
- use_request_proxy (bool, optional): Whether to use the MediaFlow proxy configuration. Defaults to True.
136
 
137
  Returns:
138
- Response: The HTTP response with the streamed content.
139
  """
140
- client = httpx.AsyncClient(
141
- follow_redirects=True,
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
- # clean up the headers to only include the necessary headers and remove acl headers
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
- logger.error(f"Internal server error while handling {method} request: {e}")
181
- return Response(status_code=502, content=f"Internal server error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- logger.exception(f"Unexpected error while processing m3u8: {e}")
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
- mpd_url: str,
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
- mpd_url (str): The URL of the MPD manifest.
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
- mpd_url,
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
- mpd_url: str,
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
- mpd_url (str): The URL of the MPD manifest.
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
- mpd_url,
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
- init_url: str,
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
- init_url (str): The URL of the initialization segment.
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(init_url, proxy_headers.request, verify_ssl, use_request_proxy)
 
 
369
  segment_content = await download_file_with_retry(
370
- segment_url, proxy_headers.request, verify_ssl=verify_ssl, use_request_proxy=use_request_proxy
 
 
 
371
  )
372
- except DownloadError as e:
373
- raise HTTPException(status_code=e.status_code, detail=f"Failed to download segment: {e.message}")
374
- return await process_segment(init_content, segment_content, mimetype, proxy_headers, key_id, key)
 
 
 
 
 
 
 
 
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 fastapi import Request, Depends, APIRouter
2
- from pydantic import HttpUrl
 
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
- d: HttpUrl,
15
- proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
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
- d (HttpUrl): The destination URL to fetch the content from.
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
- destination = str(d)
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
- d: HttpUrl,
43
- proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
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
- d (HttpUrl): The URL of the video to stream.
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, str(d), proxy_headers, verify_ssl, use_request_proxy)
62
 
63
 
64
  @proxy_router.get("/mpd/manifest.m3u8")
65
  async def manifest_endpoint(
66
  request: Request,
67
- d: HttpUrl,
68
- proxy_headers: ProxyRequestHeaders = Depends(get_proxy_headers),
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
- d (HttpUrl): The URL of the MPD manifest.
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, str(d), proxy_headers, key_id, key, verify_ssl, use_request_proxy)
90
 
91
 
92
  @proxy_router.get("/mpd/playlist.m3u8")
93
  async def playlist_endpoint(
94
  request: Request,
95
- d: HttpUrl,
96
- profile_id: str,
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
- d (HttpUrl): The URL of the MPD manifest.
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, str(d), profile_id, proxy_headers, key_id, key, verify_ssl, use_request_proxy)
120
 
121
 
122
  @proxy_router.get("/mpd/segment")
123
  async def segment_endpoint(
124
- init_url: HttpUrl,
125
- segment_url: HttpUrl,
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
- init_url (HttpUrl): The URL of the initialization segment.
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).")