Improve error handling and buffering in m3u8 processing

This commit is contained in:
mhdzumair
2025-04-23 22:23:34 +05:30
parent 86c8ee1159
commit b7d009e7c3
3 changed files with 32 additions and 43 deletions
+3
View File
@@ -3,6 +3,7 @@ import logging
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import httpx import httpx
import tenacity
from fastapi import Request, Response, HTTPException from fastapi import Request, Response, HTTPException
from starlette.background import BackgroundTask from starlette.background import BackgroundTask
@@ -52,6 +53,8 @@ def handle_exceptions(exception: Exception) -> Response:
elif isinstance(exception, DownloadError): elif isinstance(exception, DownloadError):
logger.error(f"Error downloading content: {exception}") logger.error(f"Error downloading content: {exception}")
return Response(status_code=exception.status_code, content=str(exception)) return Response(status_code=exception.status_code, content=str(exception))
elif isinstance(exception, tenacity.RetryError):
return Response(status_code=502, content="Max retries exceeded while downloading content")
else: else:
logger.exception(f"Internal server error while handling request: {exception}") logger.exception(f"Internal server error while handling request: {exception}")
return Response(status_code=502, content=f"Internal server error: {exception}") return Response(status_code=502, content=f"Internal server error: {exception}")
+6 -4
View File
@@ -33,9 +33,8 @@ class DownloadError(Exception):
def create_httpx_client(follow_redirects: bool = True, **kwargs) -> httpx.AsyncClient: def create_httpx_client(follow_redirects: bool = True, **kwargs) -> httpx.AsyncClient:
"""Creates an HTTPX client with configured proxy routing""" """Creates an HTTPX client with configured proxy routing"""
mounts = settings.transport_config.get_mounts() mounts = settings.transport_config.get_mounts()
client = httpx.AsyncClient( kwargs.setdefault("timeout", settings.transport_config.timeout)
mounts=mounts, follow_redirects=follow_redirects, timeout=settings.transport_config.timeout, **kwargs client = httpx.AsyncClient(mounts=mounts, follow_redirects=follow_redirects, **kwargs)
)
return client return client
@@ -125,9 +124,12 @@ class Streamer:
raise DownloadError( raise DownloadError(
e.response.status_code, f"HTTP error {e.response.status_code} while creating streaming response" e.response.status_code, f"HTTP error {e.response.status_code} while creating streaming response"
) )
except httpx.RequestError as e:
logger.error(f"Error creating streaming response: {e}")
raise DownloadError(502, f"Error creating streaming response: {e}")
except Exception as e: except Exception as e:
logger.error(f"Error creating streaming response: {e}") logger.error(f"Error creating streaming response: {e}")
raise raise RuntimeError(f"Error creating streaming response: {e}")
async def stream_content(self) -> typing.AsyncGenerator[bytes, None]: async def stream_content(self) -> typing.AsyncGenerator[bytes, None]:
""" """
+23 -39
View File
@@ -1,4 +1,4 @@
import logging import codecs
import re import re
from typing import AsyncGenerator from typing import AsyncGenerator
from urllib import parse from urllib import parse
@@ -57,54 +57,38 @@ class M3U8Processor:
Yields: Yields:
str: Processed lines of the m3u8 content. str: Processed lines of the m3u8 content.
""" """
buffer = b"" # Use bytes buffer instead of str buffer = "" # String buffer for decoded content
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
# Process the content chunk by chunk # Process the content chunk by chunk
async for chunk in content_iterator: async for chunk in content_iterator:
if isinstance(chunk, str): if isinstance(chunk, str):
chunk = chunk.encode("utf-8") chunk = chunk.encode("utf-8")
buffer += chunk # Incrementally decode the chunk
decoded_chunk = decoder.decode(chunk)
buffer += decoded_chunk
try: # Process complete lines
# Try to decode the current buffer with error handling lines = buffer.split("\n")
decoded_buffer = buffer.decode("utf-8", errors="replace") if len(lines) > 1:
lines = decoded_buffer.split("\n") # Process all complete lines except the last one
for line in lines[:-1]:
if line: # Skip empty lines
processed_line = await self.process_line(line, base_url)
yield processed_line + "\n"
# If we have at least one line and the buffer ends with newline, # Keep the last line in the buffer (it might be incomplete)
# we process all lines including the last one buffer = lines[-1]
if len(lines) > 1:
# Process all complete lines except the last one
for line in lines[:-1]:
if line: # Skip empty lines
processed_line = await self.process_line(line, base_url)
yield processed_line + "\n"
# If the buffer ended with a newline, the last line is also complete # Process any remaining data in the buffer plus final bytes
if decoded_buffer.endswith("\n"): final_chunk = decoder.decode(b"", final=True)
if lines[-1]: # Process the last line if it's not empty if final_chunk:
processed_line = await self.process_line(lines[-1], base_url) buffer += final_chunk
yield processed_line + "\n"
buffer = b"" # Clear the buffer
else:
# Keep the last incomplete line in the buffer
buffer = lines[-1].encode("utf-8")
except UnicodeDecodeError:
# If we can't decode the buffer, keep accumulating data
continue
# Process any remaining data in the buffer if buffer: # Process the last line if it's not empty
if buffer: processed_line = await self.process_line(buffer, base_url)
try: yield processed_line
# Final attempt to decode any remaining data
remaining = buffer.decode("utf-8", errors="replace")
if remaining:
processed_line = await self.process_line(remaining, base_url)
yield processed_line
except Exception as e:
logging.error(f"Error processing final buffer: {e}")
# Yield any remaining content with replacement characters
yield await self.process_line(buffer.decode("utf-8", errors="replace"), base_url)
async def process_line(self, line: str, base_url: str) -> str: async def process_line(self, line: str, base_url: str) -> str:
""" """