73 lines
2.4 KiB
Python
73 lines
2.4 KiB
Python
import ipaddress
|
|
import socket
|
|
import subprocess
|
|
import uuid
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
import httpx
|
|
|
|
from app.config import settings
|
|
|
|
|
|
def _validate_remote_url(url: str) -> None:
|
|
parsed = urlparse(url)
|
|
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
raise ValueError("Only HTTP or HTTPS media URLs are supported")
|
|
default_port = 443 if parsed.scheme == "https" else 80
|
|
for address in socket.getaddrinfo(parsed.hostname, parsed.port or default_port):
|
|
ip = ipaddress.ip_address(address[4][0])
|
|
if ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified:
|
|
raise ValueError("The media URL resolves to a prohibited address")
|
|
|
|
|
|
async def download_media(url: str, suffix: str, max_bytes: int) -> Path:
|
|
_validate_remote_url(url)
|
|
settings.jobs_dir.mkdir(parents=True, exist_ok=True)
|
|
destination = settings.jobs_dir / f"{uuid.uuid4().hex}{suffix}"
|
|
total = 0
|
|
timeout = httpx.Timeout(settings.media_timeout_seconds)
|
|
try:
|
|
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
|
async with client.stream("GET", url) as response:
|
|
response.raise_for_status()
|
|
with destination.open("wb") as output:
|
|
async for chunk in response.aiter_bytes(1024 * 1024):
|
|
total += len(chunk)
|
|
if total > max_bytes:
|
|
raise ValueError("Media file exceeds the configured size limit")
|
|
output.write(chunk)
|
|
if total == 0:
|
|
raise ValueError("Downloaded media file is empty")
|
|
return destination
|
|
except Exception:
|
|
destination.unlink(missing_ok=True)
|
|
raise
|
|
|
|
|
|
def decode_audio(path: Path, sample_rate: int = 16000) -> tuple[bytes, int]:
|
|
process = subprocess.run(
|
|
[
|
|
"ffmpeg",
|
|
"-v",
|
|
"error",
|
|
"-i",
|
|
str(path),
|
|
"-f",
|
|
"s16le",
|
|
"-acodec",
|
|
"pcm_s16le",
|
|
"-ac",
|
|
"1",
|
|
"-ar",
|
|
str(sample_rate),
|
|
"pipe:1",
|
|
],
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if process.returncode != 0 or not process.stdout:
|
|
error = process.stderr.decode("utf-8", errors="replace")[-500:]
|
|
raise ValueError(f"Unable to decode audio: {error}")
|
|
return process.stdout, sample_rate
|