direct download via youtube links
This commit is contained in:
@@ -54,6 +54,17 @@ async def search_youtube(q: str = Query(..., min_length=1)) -> list[dict[str, An
|
||||
raise HTTPException(status_code=502, detail=f"YouTube search failed: {exc}")
|
||||
|
||||
|
||||
@app.get("/api/youtube/resolve")
|
||||
async def resolve_youtube(url: str = Query(..., min_length=1)) -> dict[str, Any]:
|
||||
"""Resolve a pasted YouTube URL to video metadata (no download)."""
|
||||
try:
|
||||
return await youtube.resolve_youtube_url(url)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Couldn't load that video: {exc}")
|
||||
|
||||
|
||||
# ── Download ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/download")
|
||||
|
||||
@@ -65,6 +65,50 @@ def _format_duration(seconds: int | None) -> str:
|
||||
return f"{m}:{s:02d}"
|
||||
|
||||
|
||||
# ── Resolve pasted URL ───────────────────────────────────────────────────────
|
||||
|
||||
_VIDEO_ID_RE = re.compile(
|
||||
r"(?:youtube(?:-nocookie)?\.com/(?:watch\?(?:.*&)?v=|embed/|shorts/|v/)|youtu\.be/)"
|
||||
r"([A-Za-z0-9_-]{11})"
|
||||
)
|
||||
_BARE_ID_RE = re.compile(r"^[A-Za-z0-9_-]{11}$")
|
||||
|
||||
|
||||
def extract_video_id(url: str) -> str | None:
|
||||
"""Pull an 11-char YouTube video ID out of a pasted URL (or bare ID)."""
|
||||
url = url.strip()
|
||||
if _BARE_ID_RE.match(url):
|
||||
return url
|
||||
match = _VIDEO_ID_RE.search(url)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
async def resolve_youtube_url(url: str) -> dict[str, Any]:
|
||||
"""Fetch metadata for a single pasted YouTube URL, without downloading it."""
|
||||
video_id = extract_video_id(url)
|
||||
if not video_id:
|
||||
raise ValueError("Couldn't find a YouTube video ID in that URL.")
|
||||
|
||||
watch_url = f"https://www.youtube.com/watch?v={video_id}"
|
||||
ydl_opts = {"quiet": True, "no_warnings": True, "skip_download": True}
|
||||
loop = asyncio.get_event_loop()
|
||||
entry = await loop.run_in_executor(None, _yt_resolve, watch_url, ydl_opts)
|
||||
|
||||
return {
|
||||
"videoId": video_id,
|
||||
"title": entry.get("title", ""),
|
||||
"channel": entry.get("uploader") or entry.get("channel", ""),
|
||||
"duration": _format_duration(entry.get("duration")),
|
||||
"thumbnailUrl": f"https://i.ytimg.com/vi/{video_id}/mqdefault.jpg",
|
||||
"watchUrl": watch_url,
|
||||
}
|
||||
|
||||
|
||||
def _yt_resolve(url: str, opts: dict) -> dict[str, Any]:
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
return ydl.extract_info(url, download=False)
|
||||
|
||||
|
||||
# ── Download ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async def start_download(
|
||||
|
||||
Reference in New Issue
Block a user