mirror of
https://github.com/Andre-Bzk/Rezept0r.git
synced 2026-11-15 15:43:04 +01:00
fix: TikTok-Extraktion im Docker-Container (Pi-hole DNS + Windows-FFMPEG_LOCATION)
This commit is contained in:
@@ -157,9 +157,29 @@ Der Extraktions-Endpoint streamt drei Event-Typen:
|
|||||||
- **`result`** -- Fertiges Rezept als JSON (`ExtractResponse`)
|
- **`result`** -- Fertiges Rezept als JSON (`ExtractResponse`)
|
||||||
- **`error`** -- Fehlermeldung
|
- **`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
|
## 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
|
- **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
|
- **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
|
- **Bild-Upload** nach Tandoor erfolgt per `PATCH` mit Multipart -- der `Content-Type`-Header wird nicht manuell gesetzt
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class Settings(BaseSettings):
|
|||||||
app_port: int = 8000
|
app_port: int = 8000
|
||||||
tmp_dir: str = "tmp"
|
tmp_dir: str = "tmp"
|
||||||
ffmpeg_location: str = "" # Optional: full path to ffmpeg binary dir (for Windows local dev)
|
ffmpeg_location: str = "" # Optional: full path to ffmpeg binary dir (for Windows local dev)
|
||||||
|
|
||||||
@field_validator("tmp_dir", "ffmpeg_location", mode="before")
|
@field_validator("tmp_dir", "ffmpeg_location", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve_project_relative_paths(cls, value: str) -> str:
|
def _resolve_project_relative_paths(cls, value: str) -> str:
|
||||||
|
|||||||
+24
-13
@@ -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)
|
log.warning("Could not delete tmp file %s: %s", audio_path, e)
|
||||||
|
|
||||||
|
|
||||||
def _ffmpeg_args() -> list[str]:
|
def _ytdlp_extra_args() -> list[str]:
|
||||||
"""Return --ffmpeg-location args if configured, else empty list."""
|
"""Return optional yt-dlp flags from settings (ffmpeg, proxy, cookies)."""
|
||||||
loc = get_settings().ffmpeg_location
|
settings = get_settings()
|
||||||
return ["--ffmpeg-location", loc] if loc else []
|
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]:
|
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:
|
async def _get_metadata(url: str) -> dict:
|
||||||
"""Run yt-dlp --dump-json to get metadata without downloading."""
|
"""Run yt-dlp --dump-json to get metadata without downloading."""
|
||||||
stdout, stderr, returncode = await _run_yt_dlp(
|
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:
|
if returncode != 0:
|
||||||
err = stderr.decode(errors="replace")
|
err = stderr.decode(errors="replace")
|
||||||
@@ -159,7 +162,7 @@ async def _download_audio(url: str, output_path: Path) -> None:
|
|||||||
"--audio-format", "mp3",
|
"--audio-format", "mp3",
|
||||||
"--audio-quality", "5",
|
"--audio-quality", "5",
|
||||||
"--max-filesize", MAX_FILESIZE,
|
"--max-filesize", MAX_FILESIZE,
|
||||||
*_ffmpeg_args(),
|
*_ytdlp_extra_args(),
|
||||||
"--output", str(output_path).replace(".mp3", ".%(ext)s"),
|
"--output", str(output_path).replace(".mp3", ".%(ext)s"),
|
||||||
url,
|
url,
|
||||||
timeout=300,
|
timeout=300,
|
||||||
@@ -197,10 +200,18 @@ def _get_thumbnail(metadata: dict) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def _check_tiktok_error(stderr: str, url: str) -> None:
|
def _check_tiktok_error(stderr: str, url: str) -> None:
|
||||||
"""Raise a clear error for TikTok login/cookie issues."""
|
"""Raise a clear error for TikTok connectivity/auth issues."""
|
||||||
if "tiktok" in url.lower():
|
if "tiktok" not in url.lower():
|
||||||
if any(phrase in stderr.lower() for phrase in ["login", "cookie", "captcha", "403"]):
|
return
|
||||||
raise RuntimeError(
|
err_lower = stderr.lower()
|
||||||
"TikTok erfordert Cookies für dieses Video. "
|
if any(p in err_lower for p in ["connection refused", "failed to establish", "unable to download webpage"]):
|
||||||
"Bitte exportiere deine Browser-Cookies und konfiguriere yt-dlp entsprechend."
|
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
@@ -15,7 +15,6 @@ services:
|
|||||||
- TMP_DIR
|
- TMP_DIR
|
||||||
- APP_HOST
|
- APP_HOST
|
||||||
- APP_PORT
|
- APP_PORT
|
||||||
- FFMPEG_LOCATION
|
|
||||||
- APP_UID
|
- APP_UID
|
||||||
- APP_GID
|
- APP_GID
|
||||||
ports:
|
ports:
|
||||||
@@ -25,3 +24,6 @@ services:
|
|||||||
- ${REZEPTOR_DATA_DIR:-./data}:/app/data
|
- ${REZEPTOR_DATA_DIR:-./data}:/app/data
|
||||||
mem_limit: 512m
|
mem_limit: 512m
|
||||||
memswap_limit: 512m # No swap – protect SD card
|
memswap_limit: 512m # No swap – protect SD card
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
|||||||
Reference in New Issue
Block a user