fix: Rezeptverlauf-Bilder lokal speichern und Windows-Subprocess-Fix

- Bilder beim Extrahieren lokal cachen (data/images/{id}.ext) und über GET /api/history/{id}/image ausliefern; Löschen bei Verlaufseintrag-Entfernung
- history.db in ./data/ Volume verschoben, damit sie Container-Rebuilds überlebt
- yt-dlp Subprocess-Aufrufe auf Windows gefixt: run_in_executor statt asyncio.create_subprocess_exec (SelectorEventLoop-Kompatibilität)
- ./data:/app/data Docker-Volume für persistente Datenhaltung hinzugefügt
This commit is contained in:
Andre
2026-04-14 17:17:03 +02:00
parent 4de67f0192
commit c7582dafbb
13 changed files with 125 additions and 33 deletions
+10 -1
View File
@@ -11,7 +11,7 @@ from app.config import get_settings
def _db_path() -> Path:
settings = get_settings()
return Path(settings.tmp_dir).parent / "history.db"
return Path(settings.images_dir).parent / "history.db"
def init_db() -> None:
@@ -73,6 +73,15 @@ def delete_recipe(history_id: int) -> None:
conn.commit()
def update_image_url(history_id: int, image_url: str) -> None:
with sqlite3.connect(_db_path()) as conn:
conn.execute(
"UPDATE recipe_history SET image_url = ? WHERE id = ?",
(image_url, history_id),
)
conn.commit()
def mark_imported(history_id: int, tandoor_recipe_id: Optional[int]) -> None:
now = datetime.now(timezone.utc).isoformat()
with sqlite3.connect(_db_path()) as conn:
+50
View File
@@ -0,0 +1,50 @@
"""Local image caching for recipe history."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Optional
import httpx
from app.config import get_settings
log = logging.getLogger(__name__)
MIME_TO_EXT = {
"image/jpeg": "jpg",
"image/png": "png",
"image/webp": "webp",
"image/gif": "gif",
}
def images_dir() -> Path:
p = Path(get_settings().images_dir)
p.mkdir(parents=True, exist_ok=True)
return p
def find_cached_image(history_id: int) -> Optional[Path]:
"""Return the cached image Path for a history entry, or None."""
for ext in MIME_TO_EXT.values():
p = images_dir() / f"{history_id}.{ext}"
if p.exists():
return p
return None
async def download_and_cache(history_id: int, remote_url: str) -> Optional[str]:
"""Download image from remote_url, save locally, return local API path or None on failure."""
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(remote_url, follow_redirects=True)
resp.raise_for_status()
ct = resp.headers.get("content-type", "image/jpeg").split(";")[0].strip()
ext = MIME_TO_EXT.get(ct, "jpg")
dest = images_dir() / f"{history_id}.{ext}"
dest.write_bytes(resp.content)
log.info("Cached image for history %d → %s", history_id, dest)
return f"/api/history/{history_id}/image"
except Exception as exc:
log.warning("Image cache failed for history %d: %s", history_id, exc)
return None