3f1da92cf4
YouTube subtitles via OpenRouter become persistent, shareable recaps with timestamp jump links, PIN-protected creation, and Traefik deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
62 lines
1.9 KiB
Python
62 lines
1.9 KiB
Python
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
|