fix: normalize YouTube proxy URL and support dashboard fields

Rewrite https proxy URLs to http, allow HOST/PORT/USER/PASSWORD env vars,
and surface clearer errors when proxies fail on the wrong port.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-06-16 16:30:23 +02:00
parent 76de7c7a3a
commit 12cf6cab32
6 changed files with 73 additions and 6 deletions
+6 -1
View File
@@ -19,8 +19,13 @@ class Settings(BaseSettings):
app_version: str = "1.0.0"
# 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 = ""
# 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 = ""
+44 -2
View File
@@ -5,6 +5,7 @@ import logging
import re
from dataclasses import dataclass
from typing import Any
from urllib.parse import quote
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api._errors import (
@@ -72,6 +73,41 @@ def _proxy_dict() -> dict[str, str] | 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 = [
@@ -85,7 +121,7 @@ def build_proxy_config() -> ProxyConfig | None:
filter_ip_locations=locations or None,
)
proxy_url = settings.youtube_proxy.strip()
proxy_url = resolve_proxy_url()
if 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:
direct = settings.youtube_proxy.strip()
direct = resolve_proxy_url()
if direct:
return direct
config = build_proxy_config()
@@ -138,6 +174,8 @@ def _fetch_with_youtube_transcript_api(video_id: str) -> Transcript:
raise
except YouTubeRequestFailed as 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
@@ -243,7 +281,11 @@ def fetch_transcript(video_id: str) -> Transcript:
try:
return _fetch_with_ytdlp(video_id)
except ValueError:
raise
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)
raise ValueError(
"Dieses Video hat keine Captions. Dein Goldfisch kann leider nicht ins Leere starren."