import logging import time from functools import lru_cache from urllib.parse import unquote import httpx from fastapi import APIRouter, HTTPException, Query from fastapi.responses import StreamingResponse from app.models.youtube import YouTubeExtractRequest, YouTubeStreamResponse, StreamFormat logger = logging.getLogger(__name__) router = APIRouter(tags=["youtube"]) @lru_cache def _get_youtube_service(): from app.core.config import get_settings from app.services.youtube_service import YouTubeService s = get_settings() return YouTubeService(timeout=s.yt_dlp_timeout, cache_ttl=s.yt_dlp_cache_ttl) @router.post("/youtube/extract", response_model=YouTubeStreamResponse) async def extract_youtube_stream(req: YouTubeExtractRequest): from app.core.config import get_settings settings = get_settings() if not settings.youtube_proxy_enabled: raise HTTPException(status_code=503, detail="YouTube proxy is disabled") service = _get_youtube_service() started = time.monotonic() logger.info("youtube-extract-started url=%s", req.url) try: data = await service.extract_streams(req.url) except Exception as e: logger.error("youtube-extract-failed url=%s error=%s", req.url, e) raise HTTPException(status_code=500, detail=str(e)) if data.get("error"): logger.warning( "youtube-extract-error url=%s error=%s duration=%.1fs", req.url, data["error"], time.monotonic() - started, ) return YouTubeStreamResponse( video_id=data.get("video_id", ""), title=data.get("title", ""), error=data["error"], ) formats = [ StreamFormat( format_id=f.get("format_id", ""), url=f.get("url", ""), resolution=f.get("resolution"), is_audio_only=f.get("acodec", "none") != "none" and f.get("vcodec", "none") == "none", is_video_only=f.get("vcodec", "none") != "none" and f.get("acodec", "none") == "none", codec=f.get("vcodec") or f.get("acodec"), ) for f in data.get("formats", []) ] logger.info( "youtube-extract-completed url=%s video_id=%s is_live=%s fmt_count=%d duration=%.1fs", req.url, data["video_id"], data["is_live"], len(formats), time.monotonic() - started, ) return YouTubeStreamResponse( video_id=data["video_id"], title=data["title"], is_live=data["is_live"], is_upcoming=data["is_upcoming"], video_proxy_url=data.get("video_proxy_url"), audio_proxy_url=data.get("audio_proxy_url"), thumbnail_url=data.get("thumbnail_url"), formats=formats, ) @router.get("/youtube/proxy/manifest.m3u8") async def proxy_manifest(url: str = Query(..., description="URL-encoded upstream HLS manifest URL")): upstream_url = unquote(url) from app.services.hls_proxy import HLSProxyService client = httpx.AsyncClient(timeout=30.0) req = client.build_request("GET", upstream_url) upstream = await client.send(req, stream=True) if upstream.status_code != 200: await upstream.aclose() await client.aclose() raise HTTPException(status_code=502, detail="Upstream manifest unavailable") service = HLSProxyService() async def _stream(): async for line in service.rewrite_manifest(upstream_url, upstream): yield line.encode("utf-8") await upstream.aclose() await client.aclose() return StreamingResponse( _stream(), media_type="application/vnd.apple.mpegurl", headers={"access-control-allow-origin": "*"}, ) @router.get("/youtube/proxy/segment.ts") async def proxy_segment(url: str = Query(..., description="URL-encoded upstream .ts segment URL")): upstream_url = unquote(url) from app.services.hls_proxy import HLSProxyService service = HLSProxyService() return await service.proxy_segment(upstream_url)