from __future__ import annotations import re from typing import Any from urllib.parse import parse_qs, urlparse import httpx VIDEO_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{11}$") def extract_video_id(url_or_id: str) -> str: raw = url_or_id.strip() if VIDEO_ID_RE.match(raw): return raw parsed = urlparse(raw) host = (parsed.netloc or "").lower().replace("www.", "") if host in ("youtu.be",): candidate = parsed.path.lstrip("/").split("/")[0] if VIDEO_ID_RE.match(candidate): return candidate if host in ("youtube.com", "m.youtube.com", "music.youtube.com"): if parsed.path == "/watch": qs = parse_qs(parsed.query) vid = qs.get("v", [""])[0] if VIDEO_ID_RE.match(vid): return vid match = re.match(r"^/(embed|shorts|live)/([a-zA-Z0-9_-]{11})", parsed.path) if match: return match.group(2) raise ValueError("Keine gültige YouTube-URL oder Video-ID.") async def fetch_metadata(video_id: str) -> dict[str, Any]: async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.get( "https://www.youtube.com/oembed", params={"url": f"https://www.youtube.com/watch?v={video_id}", "format": "json"}, ) if resp.status_code == 404: raise ValueError("Video nicht gefunden.") resp.raise_for_status() data = resp.json() return { "video_id": video_id, "title": data.get("title") or "Unbekanntes Video", "channel": data.get("author_name") or "", "thumbnail_url": data.get("thumbnail_url") or f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg", } def youtube_watch_url(video_id: str, timestamp_seconds: int | None = None) -> str: base = f"https://www.youtube.com/watch?v={video_id}" if timestamp_seconds is not None and timestamp_seconds >= 0: return f"{base}&t={int(timestamp_seconds)}s" return base