From 66c3fc8f7ace0d9cc33f94fd55319bddf55d8f33 Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:34:58 +0200 Subject: [PATCH] fix: TikTok-Extraktion im Docker-Container (Pi-hole DNS + Windows-FFMPEG_LOCATION) --- README.md | 22 +++++++++++++++++++++- app/config.py | 1 - app/extractors/video.py | 37 ++++++++++++++++++++++++------------- docker-compose.yml | 4 +++- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b25483d..63e3dfa 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,29 @@ Der Extraktions-Endpoint streamt drei Event-Typen: - **`result`** -- Fertiges Rezept als JSON (`ExtractResponse`) - **`error`** -- Fehlermeldung +## Bekannte Probleme & Loesungen + +### TikTok: Verbindung verweigert im Docker-Container (Pi-hole) + +Wenn Pi-hole als DNS-Server im Heimnetzwerk laeuft, kann der Docker-Container TikTok-Domains nicht aufloesen — Pi-hole gibt eine Sinkhole-IP zurueck, die Verbindung wird sofort verweigert. Ausserhalb des Containers funktioniert TikTok normal, weil der Pi-Host einen anderen DNS-Pfad nutzt. + +**Loesung**: In `docker-compose.yml` einen oeffentlichen DNS-Server eintragen: + +```yaml +services: + rezeptor: + dns: + - 1.1.1.1 + - 8.8.8.8 +``` + +Danach `docker compose up -d` (kein Rebuild noetig). + +--- + ## Technische Hinweise -- **yt-dlp** wird immer als Subprocess aufgerufen (`asyncio.create_subprocess_exec`), nie ueber die Python-API, um den Event-Loop nicht zu blockieren +- **yt-dlp** wird immer als Subprocess aufgerufen (`subprocess.run` via `loop.run_in_executor()`), nie ueber die Python-API, um den Event-Loop nicht zu blockieren - **Whisper** hat ein Dateigroessen-Limit von 25 MB -- zu lange Videos fuehren zu einem Fehler - **recipe-scrapers** laedt HTML selbststaendig -- es wird die URL uebergeben, nicht vorgeladenes HTML - **Bild-Upload** nach Tandoor erfolgt per `PATCH` mit Multipart -- der `Content-Type`-Header wird nicht manuell gesetzt diff --git a/app/config.py b/app/config.py index ec7acc8..75d2eb1 100644 --- a/app/config.py +++ b/app/config.py @@ -19,7 +19,6 @@ class Settings(BaseSettings): app_port: int = 8000 tmp_dir: str = "tmp" ffmpeg_location: str = "" # Optional: full path to ffmpeg binary dir (for Windows local dev) - @field_validator("tmp_dir", "ffmpeg_location", mode="before") @classmethod def _resolve_project_relative_paths(cls, value: str) -> str: diff --git a/app/extractors/video.py b/app/extractors/video.py index 2fffab7..fbb82fb 100644 --- a/app/extractors/video.py +++ b/app/extractors/video.py @@ -118,10 +118,13 @@ async def extract_video(url: str) -> AsyncGenerator[StatusEvent | Recipe, None]: log.warning("Could not delete tmp file %s: %s", audio_path, e) -def _ffmpeg_args() -> list[str]: - """Return --ffmpeg-location args if configured, else empty list.""" - loc = get_settings().ffmpeg_location - return ["--ffmpeg-location", loc] if loc else [] +def _ytdlp_extra_args() -> list[str]: + """Return optional yt-dlp flags from settings (ffmpeg, proxy, cookies).""" + settings = get_settings() + args: list[str] = [] + if settings.ffmpeg_location and Path(settings.ffmpeg_location).exists(): + args += ["--ffmpeg-location", settings.ffmpeg_location] + return args async def _run_yt_dlp(*args: str, timeout: int) -> tuple[bytes, bytes, int]: @@ -135,7 +138,7 @@ async def _run_yt_dlp(*args: str, timeout: int) -> tuple[bytes, bytes, int]: async def _get_metadata(url: str) -> dict: """Run yt-dlp --dump-json to get metadata without downloading.""" stdout, stderr, returncode = await _run_yt_dlp( - "yt-dlp", "--dump-json", "--no-playlist", *_ffmpeg_args(), url, timeout=60 + "yt-dlp", "--dump-json", "--no-playlist", *_ytdlp_extra_args(), url, timeout=60 ) if returncode != 0: err = stderr.decode(errors="replace") @@ -159,7 +162,7 @@ async def _download_audio(url: str, output_path: Path) -> None: "--audio-format", "mp3", "--audio-quality", "5", "--max-filesize", MAX_FILESIZE, - *_ffmpeg_args(), + *_ytdlp_extra_args(), "--output", str(output_path).replace(".mp3", ".%(ext)s"), url, timeout=300, @@ -197,10 +200,18 @@ def _get_thumbnail(metadata: dict) -> str | None: def _check_tiktok_error(stderr: str, url: str) -> None: - """Raise a clear error for TikTok login/cookie issues.""" - if "tiktok" in url.lower(): - if any(phrase in stderr.lower() for phrase in ["login", "cookie", "captcha", "403"]): - raise RuntimeError( - "TikTok erfordert Cookies für dieses Video. " - "Bitte exportiere deine Browser-Cookies und konfiguriere yt-dlp entsprechend." - ) + """Raise a clear error for TikTok connectivity/auth issues.""" + if "tiktok" not in url.lower(): + return + err_lower = stderr.lower() + if any(p in err_lower for p in ["connection refused", "failed to establish", "unable to download webpage"]): + raise RuntimeError( + "TikTok ist vom Server nicht erreichbar (Verbindung verweigert). " + "Möglicherweise blockiert die installierte yt-dlp-Version TikTok. " + "Bitte Docker-Image neu bauen: docker compose up -d --build" + ) + if any(p in err_lower for p in ["login", "cookie", "captcha", "403"]): + raise RuntimeError( + "TikTok erfordert Cookies für dieses Video. " + "Exportiere Browser-Cookies und setze YTDLP_COOKIES_FILE=/pfad/cookies.txt in der .env." + ) diff --git a/docker-compose.yml b/docker-compose.yml index ff44bc7..048f4dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: - TMP_DIR - APP_HOST - APP_PORT - - FFMPEG_LOCATION - APP_UID - APP_GID ports: @@ -25,3 +24,6 @@ services: - ${REZEPTOR_DATA_DIR:-./data}:/app/data mem_limit: 512m memswap_limit: 512m # No swap – protect SD card + dns: + - 1.1.1.1 + - 8.8.8.8