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:
@@ -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
|
||||
Reference in New Issue
Block a user