Compare commits
1 Commits
76de7c7a3a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 12cf6cab32 |
+7
-1
@@ -10,8 +10,14 @@ OPENROUTER_API_KEY=
|
|||||||
OPENROUTER_MODEL=google/gemini-2.5-flash
|
OPENROUTER_MODEL=google/gemini-2.5-flash
|
||||||
|
|
||||||
# YouTube IP-Block umgehen (optional)
|
# YouTube IP-Block umgehen (optional)
|
||||||
# Statischer Proxy aus dem Dashboard, z.B. http://USER:PASS@HOST:PORT
|
# Option A — volle URL (http:// + Port aus Dashboard, z.B. 6641):
|
||||||
|
# YOUTUBE_PROXY=http://USER:PASS@64.137.96.74:6641
|
||||||
YOUTUBE_PROXY=
|
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):
|
# Alternativ Webshare *Residential* (kostenpflichtig, rotating — NICHT Free/Proxy-Server):
|
||||||
# WEBSHARE_PROXY_USERNAME=
|
# WEBSHARE_PROXY_USERNAME=
|
||||||
# WEBSHARE_PROXY_PASSWORD=
|
# WEBSHARE_PROXY_PASSWORD=
|
||||||
|
|||||||
@@ -69,8 +69,14 @@ Traefik übernimmt TLS via `myresolver`. Persistente Daten liegen in `./data/`.
|
|||||||
YouTube blockiert oft Server-IPs (Hetzner, AWS, …). Optional in `.env`:
|
YouTube blockiert oft Server-IPs (Hetzner, AWS, …). Optional in `.env`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Statischer Proxy (z.B. aus Webshare „Proxy Server“ / Dashboard-Zeile)
|
# Volle URL — http:// und Port aus Dashboard (z.B. 6641, NICHT 443)
|
||||||
YOUTUBE_PROXY=http://USER:PASS@HOST:PORT
|
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.
|
Für yt-dlp-Fallback und VTT-Downloads wird dieselbe URL genutzt.
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ services:
|
|||||||
- 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=${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_USERNAME=${WEBSHARE_PROXY_USERNAME:-}
|
||||||
- WEBSHARE_PROXY_PASSWORD=${WEBSHARE_PROXY_PASSWORD:-}
|
- WEBSHARE_PROXY_PASSWORD=${WEBSHARE_PROXY_PASSWORD:-}
|
||||||
- WEBSHARE_PROXY_LOCATIONS=${WEBSHARE_PROXY_LOCATIONS:-}
|
- WEBSHARE_PROXY_LOCATIONS=${WEBSHARE_PROXY_LOCATIONS:-}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ services:
|
|||||||
- 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=${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_USERNAME=${WEBSHARE_PROXY_USERNAME:-}
|
||||||
- WEBSHARE_PROXY_PASSWORD=${WEBSHARE_PROXY_PASSWORD:-}
|
- WEBSHARE_PROXY_PASSWORD=${WEBSHARE_PROXY_PASSWORD:-}
|
||||||
- WEBSHARE_PROXY_LOCATIONS=${WEBSHARE_PROXY_LOCATIONS:-}
|
- WEBSHARE_PROXY_LOCATIONS=${WEBSHARE_PROXY_LOCATIONS:-}
|
||||||
|
|||||||
+6
-1
@@ -19,8 +19,13 @@ class Settings(BaseSettings):
|
|||||||
app_version: str = "1.0.0"
|
app_version: str = "1.0.0"
|
||||||
|
|
||||||
# YouTube transcript proxy (optional — for IP blocks)
|
# YouTube transcript proxy (optional — for IP blocks)
|
||||||
# Single URL used for HTTP+HTTPS, e.g. http://user:pass@host:port
|
# Option A: full URL — MUST include port, use http:// (not https://)
|
||||||
youtube_proxy: str = ""
|
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 rotating residential (paid package — NOT free "Proxy Server" tier)
|
||||||
webshare_proxy_username: str = ""
|
webshare_proxy_username: str = ""
|
||||||
webshare_proxy_password: str = ""
|
webshare_proxy_password: str = ""
|
||||||
|
|||||||
+44
-2
@@ -5,6 +5,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
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 (
|
||||||
@@ -72,6 +73,41 @@ def _proxy_dict() -> dict[str, str] | None:
|
|||||||
return config.to_requests_dict()
|
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:
|
def build_proxy_config() -> ProxyConfig | None:
|
||||||
if settings.webshare_proxy_username and settings.webshare_proxy_password:
|
if settings.webshare_proxy_username and settings.webshare_proxy_password:
|
||||||
locations = [
|
locations = [
|
||||||
@@ -85,7 +121,7 @@ def build_proxy_config() -> ProxyConfig | None:
|
|||||||
filter_ip_locations=locations or None,
|
filter_ip_locations=locations or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
proxy_url = settings.youtube_proxy.strip()
|
proxy_url = resolve_proxy_url()
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
return GenericProxyConfig(http_url=proxy_url, https_url=proxy_url)
|
return GenericProxyConfig(http_url=proxy_url, https_url=proxy_url)
|
||||||
|
|
||||||
@@ -93,7 +129,7 @@ def build_proxy_config() -> ProxyConfig | None:
|
|||||||
|
|
||||||
|
|
||||||
def proxy_url_for_ytdlp() -> str | None:
|
def proxy_url_for_ytdlp() -> str | None:
|
||||||
direct = settings.youtube_proxy.strip()
|
direct = resolve_proxy_url()
|
||||||
if direct:
|
if direct:
|
||||||
return direct
|
return direct
|
||||||
config = build_proxy_config()
|
config = build_proxy_config()
|
||||||
@@ -138,6 +174,8 @@ def _fetch_with_youtube_transcript_api(video_id: str) -> Transcript:
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -243,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