fix: TikTok-Extraktion im Docker-Container (Pi-hole DNS + Windows-FFMPEG_LOCATION)

This commit is contained in:
Andre
2026-04-16 18:34:58 +02:00
parent 895e2e588f
commit 66c3fc8f7a
4 changed files with 48 additions and 16 deletions
+21 -1
View File
@@ -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
-1
View File
@@ -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:
+24 -13
View File
@@ -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."
)
+3 -1
View File
@@ -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