From 7bb58693dd000e96e7ce777d5acd8902528e64b2 Mon Sep 17 00:00:00 2001 From: Khoa Vo Date: Thu, 1 Jan 2026 15:28:28 +0700 Subject: [PATCH] fix: improve stream API for Docker/NAS - add SSL certs, retries, geo bypass --- Dockerfile | 4 +++- backend/api/routes.py | 48 +++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index d5ddd92..a26f6b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ FROM python:3.11-slim -# Install Node.js +# Install Node.js and dependencies RUN apt-get update && apt-get install -y \ curl \ gnupg \ ffmpeg \ + ca-certificates \ + && update-ca-certificates \ && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* diff --git a/backend/api/routes.py b/backend/api/routes.py index b26861e..22f55a9 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -615,15 +615,24 @@ async def stream_audio(id: str): print(f"DEBUG: Fetching new stream URL for '{id}'") url = f"https://www.youtube.com/watch?v={id}" ydl_opts = { - 'format': 'bestaudio[ext=m4a]/best[ext=mp4]/best', # Prefer m4a/aac for iOS + 'format': 'bestaudio[ext=m4a]/bestaudio[ext=webm]/bestaudio/best', 'quiet': True, 'noplaylist': True, + 'nocheckcertificate': True, # Bypass SSL issues in Docker + 'geo_bypass': True, # Bypass geo restrictions + 'socket_timeout': 30, # Timeout for sockets + 'retries': 3, # Retry on transient errors } # Extract direct URL - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(url, download=False) - stream_url = info.get('url') + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + stream_url = info.get('url') + print(f"DEBUG: Got stream URL format: {info.get('format')}, ext: {info.get('ext')}") + except Exception as ydl_error: + print(f"DEBUG: yt-dlp extraction error: {type(ydl_error).__name__}: {str(ydl_error)}") + raise ydl_error if stream_url: # Cache for 1 hour (3600 seconds) - URLs expire @@ -634,24 +643,27 @@ async def stream_audio(id: str): # Stream the content def iterfile(): - # Verify if URL is still valid by making a HEAD request or handling stream error - # For simplicity, we just try to stream. If 403, we might need to invalidate, - # but that logic is complex for this method. - with requests.get(stream_url, stream=True) as r: - r.raise_for_status() # Check for 403 - # Use smaller chunks (64KB) for better TTFB (Time To First Byte) - for chunk in r.iter_content(chunk_size=64*1024): - yield chunk + try: + with requests.get(stream_url, stream=True, timeout=30) as r: + r.raise_for_status() + for chunk in r.iter_content(chunk_size=64*1024): + yield chunk + except requests.exceptions.HTTPError as http_err: + print(f"DEBUG: Stream HTTP Error: {http_err}") + # Invalidate cache on 403 + if http_err.response.status_code == 403: + cache.delete(cache_key) + raise - # Note: We return audio/mpeg, but it might be opus/webm. - # Browsers are usually smart enough to sniff. return StreamingResponse(iterfile(), media_type="audio/mpeg") + except HTTPException: + raise except Exception as e: - print(f"Stream Error: {e}") - # If cached URL failed (likely 403), we could try to invalidate here, - # but for now we just return error. - raise HTTPException(status_code=500, detail=str(e)) + import traceback + print(f"Stream Error for ID '{id}': {type(e).__name__}: {str(e)}") + print(traceback.format_exc()) + raise HTTPException(status_code=500, detail=f"Stream error: {type(e).__name__}: {str(e)}") @router.get("/download") async def download_audio(id: str, title: str = "audio"):