Compare commits
3 Commits
3f1da92cf4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 12cf6cab32 | |||
| 76de7c7a3a | |||
| 5b8042b173 |
+13
-2
@@ -9,5 +9,16 @@ JWT_EXPIRE_DAYS=90
|
|||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
OPENROUTER_MODEL=google/gemini-2.5-flash
|
OPENROUTER_MODEL=google/gemini-2.5-flash
|
||||||
|
|
||||||
# Optional lokal
|
# YouTube IP-Block umgehen (optional)
|
||||||
# APP_URL=http://localhost:8000
|
# Option A — volle URL (http:// + Port aus Dashboard, z.B. 6641):
|
||||||
|
# YOUTUBE_PROXY=http://USER:PASS@64.137.96.74:6641
|
||||||
|
YOUTUBE_PROXY=
|
||||||
|
# Option B — Felder aus dem Dashboard (einfacher, kein URL-Format):
|
||||||
|
# YOUTUBE_PROXY_HOST=64.137.96.74
|
||||||
|
# YOUTUBE_PROXY_PORT=6641
|
||||||
|
# YOUTUBE_PROXY_USER=
|
||||||
|
# YOUTUBE_PROXY_PASSWORD=
|
||||||
|
# Alternativ Webshare *Residential* (kostenpflichtig, rotating — NICHT Free/Proxy-Server):
|
||||||
|
# WEBSHARE_PROXY_USERNAME=
|
||||||
|
# WEBSHARE_PROXY_PASSWORD=
|
||||||
|
# WEBSHARE_PROXY_LOCATIONS=de,es
|
||||||
|
|||||||
@@ -64,6 +64,33 @@ Traefik übernimmt TLS via `myresolver`. Persistente Daten liegen in `./data/`.
|
|||||||
| `POST /api/recaps` | JWT | Recap erstellen |
|
| `POST /api/recaps` | JWT | Recap erstellen |
|
||||||
| `POST /api/recaps/{video_id}/regenerate` | JWT | Neu generieren |
|
| `POST /api/recaps/{video_id}/regenerate` | JWT | Neu generieren |
|
||||||
|
|
||||||
|
## YouTube IP-Block / Proxy
|
||||||
|
|
||||||
|
YouTube blockiert oft Server-IPs (Hetzner, AWS, …). Optional in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Volle URL — http:// und Port aus Dashboard (z.B. 6641, NICHT 443)
|
||||||
|
YOUTUBE_PROXY=http://USER:PASS@HOST:6641
|
||||||
|
|
||||||
|
# Oder einzelne Felder aus dem Dashboard:
|
||||||
|
YOUTUBE_PROXY_HOST=HOST
|
||||||
|
YOUTUBE_PROXY_PORT=6641
|
||||||
|
YOUTUBE_PROXY_USER=USER
|
||||||
|
YOUTUBE_PROXY_PASSWORD=PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Für yt-dlp-Fallback und VTT-Downloads wird dieselbe URL genutzt.
|
||||||
|
|
||||||
|
**Webshare-Hinweis:** Der kostenlose Tier und „Proxy Server“ (einzelne IP wie im Dashboard) funktionieren *manchmal* für wenige Recaps pro Tag — laut `youtube-transcript-api` aber unzuverlässig. Rotating **Residential** (kostenpflichtig) ist die robuste Lösung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WEBSHARE_PROXY_USERNAME=...
|
||||||
|
WEBSHARE_PROXY_PASSWORD=...
|
||||||
|
WEBSHARE_PROXY_LOCATIONS=de
|
||||||
|
```
|
||||||
|
|
||||||
|
Öffentliche Gratis-Proxy-Listen: nein (langsam, unsicher, meist schon gebannt).
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- FastAPI + SQLite
|
- FastAPI + SQLite
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ services:
|
|||||||
- JWT_SECRET=${JWT_SECRET:-dev-secret-change-me}
|
- JWT_SECRET=${JWT_SECRET:-dev-secret-change-me}
|
||||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
|
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
|
||||||
- OPENROUTER_MODEL=${OPENROUTER_MODEL:-google/gemini-2.5-flash}
|
- OPENROUTER_MODEL=${OPENROUTER_MODEL:-google/gemini-2.5-flash}
|
||||||
|
- YOUTUBE_PROXY=${YOUTUBE_PROXY:-}
|
||||||
|
- YOUTUBE_PROXY_HOST=${YOUTUBE_PROXY_HOST:-}
|
||||||
|
- YOUTUBE_PROXY_PORT=${YOUTUBE_PROXY_PORT:-0}
|
||||||
|
- YOUTUBE_PROXY_USER=${YOUTUBE_PROXY_USER:-}
|
||||||
|
- YOUTUBE_PROXY_PASSWORD=${YOUTUBE_PROXY_PASSWORD:-}
|
||||||
|
- WEBSHARE_PROXY_USERNAME=${WEBSHARE_PROXY_USERNAME:-}
|
||||||
|
- WEBSHARE_PROXY_PASSWORD=${WEBSHARE_PROXY_PASSWORD:-}
|
||||||
|
- WEBSHARE_PROXY_LOCATIONS=${WEBSHARE_PROXY_LOCATIONS:-}
|
||||||
- DB_PATH=/data/ytrecap.sqlite
|
- DB_PATH=/data/ytrecap.sqlite
|
||||||
- APP_URL=http://localhost:8000
|
- APP_URL=http://localhost:8000
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ services:
|
|||||||
- JWT_EXPIRE_DAYS=${JWT_EXPIRE_DAYS:-90}
|
- JWT_EXPIRE_DAYS=${JWT_EXPIRE_DAYS:-90}
|
||||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
||||||
- OPENROUTER_MODEL=${OPENROUTER_MODEL:-google/gemini-2.5-flash}
|
- OPENROUTER_MODEL=${OPENROUTER_MODEL:-google/gemini-2.5-flash}
|
||||||
|
- YOUTUBE_PROXY=${YOUTUBE_PROXY:-}
|
||||||
|
- YOUTUBE_PROXY_HOST=${YOUTUBE_PROXY_HOST:-}
|
||||||
|
- YOUTUBE_PROXY_PORT=${YOUTUBE_PROXY_PORT:-0}
|
||||||
|
- YOUTUBE_PROXY_USER=${YOUTUBE_PROXY_USER:-}
|
||||||
|
- YOUTUBE_PROXY_PASSWORD=${YOUTUBE_PROXY_PASSWORD:-}
|
||||||
|
- WEBSHARE_PROXY_USERNAME=${WEBSHARE_PROXY_USERNAME:-}
|
||||||
|
- WEBSHARE_PROXY_PASSWORD=${WEBSHARE_PROXY_PASSWORD:-}
|
||||||
|
- WEBSHARE_PROXY_LOCATIONS=${WEBSHARE_PROXY_LOCATIONS:-}
|
||||||
- DB_PATH=/data/ytrecap.sqlite
|
- DB_PATH=/data/ytrecap.sqlite
|
||||||
- APP_URL=https://ytrecap.schwenk.online
|
- APP_URL=https://ytrecap.schwenk.online
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
|
|||||||
+30
-2
@@ -130,15 +130,43 @@ function wirePinForm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireCreate() {
|
const GOLDFISH_TAPS = 5;
|
||||||
document.getElementById("newRecapBtn").addEventListener("click", async () => {
|
const GOLDFISH_RESET_MS = 2000;
|
||||||
|
|
||||||
|
async function openCreateFlow() {
|
||||||
try {
|
try {
|
||||||
await waitForPinLogin();
|
await waitForPinLogin();
|
||||||
openCreateModal();
|
openCreateModal();
|
||||||
} catch {
|
} catch {
|
||||||
/* cancelled */
|
/* cancelled */
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireGoldfishSecret() {
|
||||||
|
const logo = document.getElementById("goldfishLogo");
|
||||||
|
let taps = 0;
|
||||||
|
let resetTimer = null;
|
||||||
|
|
||||||
|
logo.addEventListener("click", () => {
|
||||||
|
taps += 1;
|
||||||
|
logo.classList.add("logoWiggle");
|
||||||
|
window.setTimeout(() => logo.classList.remove("logoWiggle"), 300);
|
||||||
|
|
||||||
|
if (resetTimer) window.clearTimeout(resetTimer);
|
||||||
|
resetTimer = window.setTimeout(() => {
|
||||||
|
taps = 0;
|
||||||
|
}, GOLDFISH_RESET_MS);
|
||||||
|
|
||||||
|
if (taps >= GOLDFISH_TAPS) {
|
||||||
|
taps = 0;
|
||||||
|
if (resetTimer) window.clearTimeout(resetTimer);
|
||||||
|
openCreateFlow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireCreate() {
|
||||||
|
wireGoldfishSecret();
|
||||||
document.getElementById("createCancel").addEventListener("click", () => createModal.close());
|
document.getElementById("createCancel").addEventListener("click", () => createModal.close());
|
||||||
document.getElementById("createForm").addEventListener("submit", async (e) => {
|
document.getElementById("createForm").addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
+1
-2
@@ -18,13 +18,12 @@
|
|||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="heroInner">
|
<div class="heroInner">
|
||||||
<div class="logoRow">
|
<div class="logoRow">
|
||||||
<img src="/icon.svg" alt="" class="logo" width="56" height="56">
|
<img src="/icon.svg" alt="" class="logo" id="goldfishLogo" width="56" height="56" title="Goldfish Recap">
|
||||||
<div>
|
<div>
|
||||||
<h1>Goldfish Recap</h1>
|
<h1>Goldfish Recap</h1>
|
||||||
<p class="tagline">Second Screen? Kein Problem. Dein Gehirn hat abgespeichert: nein. Wir: ja.</p>
|
<p class="tagline">Second Screen? Kein Problem. Dein Gehirn hat abgespeichert: nein. Wir: ja.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary" id="newRecapBtn">+ Neues Recap</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ a {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2));
|
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2));
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoWiggle {
|
||||||
|
animation: goldfishWiggle 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goldfishWiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(-8deg) scale(1.05); }
|
||||||
|
75% { transform: rotate(8deg) scale(1.05); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
|
|||||||
@@ -18,5 +18,18 @@ class Settings(BaseSettings):
|
|||||||
app_url: str = "http://localhost:8000"
|
app_url: str = "http://localhost:8000"
|
||||||
app_version: str = "1.0.0"
|
app_version: str = "1.0.0"
|
||||||
|
|
||||||
|
# YouTube transcript proxy (optional — for IP blocks)
|
||||||
|
# Option A: full URL — MUST include port, use http:// (not https://)
|
||||||
|
youtube_proxy: str = ""
|
||||||
|
# Option B: dashboard fields (Webshare etc.) — combined into a proxy URL
|
||||||
|
youtube_proxy_host: str = ""
|
||||||
|
youtube_proxy_port: int = 0
|
||||||
|
youtube_proxy_user: str = ""
|
||||||
|
youtube_proxy_password: str = ""
|
||||||
|
# Webshare rotating residential (paid package — NOT free "Proxy Server" tier)
|
||||||
|
webshare_proxy_username: str = ""
|
||||||
|
webshare_proxy_password: str = ""
|
||||||
|
webshare_proxy_locations: str = "" # comma-separated, e.g. de,es
|
||||||
|
|
||||||
|
|
||||||
settings = Settings() # type: ignore[call-arg]
|
settings = Settings() # type: ignore[call-arg]
|
||||||
|
|||||||
+125
-4
@@ -4,14 +4,21 @@ import hashlib
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from youtube_transcript_api import YouTubeTranscriptApi
|
from youtube_transcript_api import YouTubeTranscriptApi
|
||||||
from youtube_transcript_api._errors import (
|
from youtube_transcript_api._errors import (
|
||||||
|
IpBlocked,
|
||||||
NoTranscriptFound,
|
NoTranscriptFound,
|
||||||
|
RequestBlocked,
|
||||||
TranscriptsDisabled,
|
TranscriptsDisabled,
|
||||||
VideoUnavailable,
|
VideoUnavailable,
|
||||||
YouTubeRequestFailed,
|
YouTubeRequestFailed,
|
||||||
)
|
)
|
||||||
|
from youtube_transcript_api.proxies import GenericProxyConfig, ProxyConfig, WebshareProxyConfig
|
||||||
|
|
||||||
|
from server.settings import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -59,18 +66,116 @@ def _normalize_ytt_segments(raw: list[dict]) -> list[TranscriptSegment]:
|
|||||||
return [TranscriptSegment(start=float(item["start"]), text=str(item["text"])) for item in raw]
|
return [TranscriptSegment(start=float(item["start"]), text=str(item["text"])) for item in raw]
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_dict() -> dict[str, str] | None:
|
||||||
|
config = build_proxy_config()
|
||||||
|
if config is None:
|
||||||
|
return None
|
||||||
|
return config.to_requests_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_proxy_url(url: str) -> str:
|
||||||
|
"""Normalize common proxy URL mistakes (https scheme, missing port hint)."""
|
||||||
|
cleaned = url.strip()
|
||||||
|
if cleaned.startswith("https://"):
|
||||||
|
cleaned = "http://" + cleaned[len("https://") :]
|
||||||
|
logger.info("YOUTUBE_PROXY: https:// → http:// (Proxy-Protokoll ist HTTP)")
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_proxy_url() -> str | None:
|
||||||
|
if settings.youtube_proxy.strip():
|
||||||
|
return normalize_proxy_url(settings.youtube_proxy)
|
||||||
|
|
||||||
|
host = settings.youtube_proxy_host.strip()
|
||||||
|
port = settings.youtube_proxy_port
|
||||||
|
user = settings.youtube_proxy_user.strip()
|
||||||
|
password = settings.youtube_proxy_password
|
||||||
|
if host and port and user and password:
|
||||||
|
return f"http://{quote(user, safe='')}:{quote(password, safe='')}@{host}:{port}"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_error_message(exc: Exception) -> str:
|
||||||
|
msg = str(exc)
|
||||||
|
if "Connection refused" in msg and ":443" in msg:
|
||||||
|
return (
|
||||||
|
"Proxy-Verbindung fehlgeschlagen (Port 443). "
|
||||||
|
"Webshare-Dashboard-Proxies nutzen meist einen anderen Port (z.B. 6641). "
|
||||||
|
"Setze YOUTUBE_PROXY=http://USER:PASS@HOST:PORT oder "
|
||||||
|
"YOUTUBE_PROXY_HOST/PORT/USER/PASSWORD — Schema http://, nicht https://."
|
||||||
|
)
|
||||||
|
return f"Proxy-Verbindung fehlgeschlagen: {exc}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_proxy_config() -> ProxyConfig | None:
|
||||||
|
if settings.webshare_proxy_username and settings.webshare_proxy_password:
|
||||||
|
locations = [
|
||||||
|
code.strip().lower()
|
||||||
|
for code in settings.webshare_proxy_locations.split(",")
|
||||||
|
if code.strip()
|
||||||
|
]
|
||||||
|
return WebshareProxyConfig(
|
||||||
|
proxy_username=settings.webshare_proxy_username,
|
||||||
|
proxy_password=settings.webshare_proxy_password,
|
||||||
|
filter_ip_locations=locations or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
proxy_url = resolve_proxy_url()
|
||||||
|
if proxy_url:
|
||||||
|
return GenericProxyConfig(http_url=proxy_url, https_url=proxy_url)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_url_for_ytdlp() -> str | None:
|
||||||
|
direct = resolve_proxy_url()
|
||||||
|
if direct:
|
||||||
|
return direct
|
||||||
|
config = build_proxy_config()
|
||||||
|
if config is None:
|
||||||
|
return None
|
||||||
|
proxies = config.to_requests_dict()
|
||||||
|
return proxies.get("https") or proxies.get("http")
|
||||||
|
|
||||||
|
|
||||||
|
def build_youtube_transcript_api() -> YouTubeTranscriptApi:
|
||||||
|
config = build_proxy_config()
|
||||||
|
if config is None:
|
||||||
|
return YouTubeTranscriptApi()
|
||||||
|
return YouTubeTranscriptApi(proxy_config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_message() -> str:
|
||||||
|
if build_proxy_config() is not None:
|
||||||
|
return (
|
||||||
|
"YouTube blockiert auch über den Proxy. "
|
||||||
|
"Statische/datacenter Proxies halten oft nicht lange — "
|
||||||
|
"für zuverlässigere Ergebnisse rotating residential nutzen."
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"YouTube blockiert deine Server-IP. "
|
||||||
|
"Setze YOUTUBE_PROXY in der .env (siehe README)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _fetch_with_youtube_transcript_api(video_id: str) -> Transcript:
|
def _fetch_with_youtube_transcript_api(video_id: str) -> Transcript:
|
||||||
api = YouTubeTranscriptApi()
|
api = build_youtube_transcript_api()
|
||||||
try:
|
try:
|
||||||
fetched = api.fetch(video_id, languages=list(PREFERRED_LANGUAGES))
|
fetched = api.fetch(video_id, languages=list(PREFERRED_LANGUAGES))
|
||||||
segments = _normalize_ytt_segments(
|
segments = _normalize_ytt_segments(
|
||||||
[{"start": s.start, "text": s.text} for s in fetched]
|
[{"start": s.start, "text": s.text} for s in fetched]
|
||||||
)
|
)
|
||||||
return Transcript(language=fetched.language_code, segments=segments, source="youtube-transcript-api")
|
return Transcript(language=fetched.language_code, segments=segments, source="youtube-transcript-api")
|
||||||
|
except (RequestBlocked, IpBlocked) as exc:
|
||||||
|
logger.warning("YouTube IP block for %s: %s", video_id, exc)
|
||||||
|
raise ValueError(_blocked_message()) from exc
|
||||||
except (TranscriptsDisabled, NoTranscriptFound, VideoUnavailable):
|
except (TranscriptsDisabled, NoTranscriptFound, VideoUnavailable):
|
||||||
raise
|
raise
|
||||||
except YouTubeRequestFailed as exc:
|
except YouTubeRequestFailed as exc:
|
||||||
logger.warning("youtube-transcript-api failed for %s: %s", video_id, exc)
|
logger.warning("youtube-transcript-api failed for %s: %s", video_id, exc)
|
||||||
|
if "ProxyError" in str(exc.reason) or "Unable to connect to proxy" in str(exc.reason):
|
||||||
|
raise ValueError(_proxy_error_message(exc)) from exc
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +209,7 @@ def _fetch_with_ytdlp(video_id: str) -> Transcript:
|
|||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
|
||||||
url = f"https://www.youtube.com/watch?v={video_id}"
|
url = f"https://www.youtube.com/watch?v={video_id}"
|
||||||
opts: dict = {
|
opts: dict[str, Any] = {
|
||||||
"skip_download": True,
|
"skip_download": True,
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": True,
|
"no_warnings": True,
|
||||||
@@ -113,6 +218,10 @@ def _fetch_with_ytdlp(video_id: str) -> Transcript:
|
|||||||
"subtitleslangs": list(PREFERRED_LANGUAGES),
|
"subtitleslangs": list(PREFERRED_LANGUAGES),
|
||||||
"subtitlesformat": "vtt",
|
"subtitlesformat": "vtt",
|
||||||
}
|
}
|
||||||
|
proxy = proxy_url_for_ytdlp()
|
||||||
|
if proxy:
|
||||||
|
opts["proxy"] = proxy
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||||
info = ydl.extract_info(url, download=False)
|
info = ydl.extract_info(url, download=False)
|
||||||
|
|
||||||
@@ -145,9 +254,17 @@ def _fetch_with_ytdlp(video_id: str) -> Transcript:
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
resp = httpx.get(vtt_url, timeout=30.0, follow_redirects=True)
|
client_kwargs: dict[str, Any] = {"timeout": 30.0, "follow_redirects": True}
|
||||||
|
proxies = _proxy_dict()
|
||||||
|
if proxies:
|
||||||
|
client_kwargs["proxy"] = proxies.get("https") or proxies.get("http")
|
||||||
|
|
||||||
|
with httpx.Client(**client_kwargs) as client:
|
||||||
|
resp = client.get(vtt_url)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
segments = _parse_vtt(resp.text)
|
vtt_text = resp.text
|
||||||
|
|
||||||
|
segments = _parse_vtt(vtt_text)
|
||||||
if not segments:
|
if not segments:
|
||||||
raise ValueError("Untertitel konnten nicht geparst werden.")
|
raise ValueError("Untertitel konnten nicht geparst werden.")
|
||||||
|
|
||||||
@@ -164,7 +281,11 @@ def fetch_transcript(video_id: str) -> Transcript:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return _fetch_with_ytdlp(video_id)
|
return _fetch_with_ytdlp(video_id)
|
||||||
|
except ValueError:
|
||||||
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
if "ProxyError" in type(exc).__name__ or "Unable to connect to proxy" in str(exc):
|
||||||
|
raise ValueError(_proxy_error_message(exc)) from exc
|
||||||
logger.exception("yt-dlp transcript fallback failed for %s", video_id)
|
logger.exception("yt-dlp transcript fallback failed for %s", video_id)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Dieses Video hat keine Captions. Dein Goldfisch kann leider nicht ins Leere starren."
|
"Dieses Video hat keine Captions. Dein Goldfisch kann leider nicht ins Leere starren."
|
||||||
|
|||||||
Reference in New Issue
Block a user