124 lines
4.0 KiB
Python
124 lines
4.0 KiB
Python
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)
|