feat: add Goldfish Recap YouTube summary PWA

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>
This commit is contained in:
Frank Schwenk
2026-06-16 15:49:24 +02:00
commit 3f1da92cf4
27 changed files with 2070 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
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