Files
photoBooth/backend/api/admin_gallery_api.py
T
admin 4592f20be3
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
fix: mtime index dans processed_full, print 404, galerie full-width
2026-07-17 16:29:06 +02:00

261 lines
9.6 KiB
Python

"""API galerie admin — impression et suppression de photos."""
import logging
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Request, Query
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
router = APIRouter()
# ── Helpers ───────────────────────────────────────────────────────────────────
def _is_image(item: dict) -> bool:
t = item.get("type", item.get("mediaitem_type", "image"))
return str(t).lower() in ("image", "still", "photo")
def _get_id(item: dict) -> str:
return str(item.get("id", item.get("filename", item.get("uid", ""))))
def _find_file(media_dir: str, photo_id: str) -> Path | None:
"""Cherche un fichier image correspondant à l'identifiant.
Cherche dans media_dir, processed_full/ et les chemins photobooth standard.
"""
base = Path(media_dir)
candidates = [
base,
base / "processed_full",
Path("/home/pi/photobooth-data/media/processed_full"),
Path("/home/pi/photobooth-data/media"),
]
for d in candidates:
if not d.exists():
continue
for ext in (".jpg", ".jpeg", ".png"):
f = d / f"{photo_id}{ext}"
if f.exists():
return f
return None
def _build_mtime_index(media_dir: Path) -> dict[str, float]:
"""Construit un index {stem: mtime, name: mtime} en cherchant dans les dossiers connus."""
index: dict[str, float] = {}
checked: set[Path] = set()
candidates = [
media_dir,
media_dir / "processed_full",
Path("/home/pi/photobooth-data/media/processed_full"),
Path("/home/pi/photobooth-data/media"),
]
for d in candidates:
if not d.exists() or d in checked:
continue
checked.add(d)
for f in d.iterdir():
if f.is_file() and f.suffix.lower() in (".jpg", ".jpeg", ".png"):
mt = f.stat().st_mtime
index[f.name] = mt
index[f.stem] = mt
return index
def _require_auth(request: Request):
return request.session.get("authenticated") is True
# ── Photos ────────────────────────────────────────────────────────────────────
@router.get("/admin/api/gallery/photos")
async def admin_get_photos(
request: Request,
page: int = Query(default=1, ge=1),
limit: int = Query(default=24, ge=1, le=100),
event_slug: str = Query(default=None),
date_from: float = Query(default=None),
date_to: float = Query(default=None),
):
"""Liste des photos pour la galerie admin, annotées avec leurs demandes d'impression."""
if not _require_auth(request):
return JSONResponse({"error": "Non authentifié"}, status_code=401)
pb = request.app.state.photobooth_service
printer_svc = request.app.state.printer_service
cfg = request.app.state.config
event_svc = getattr(request.app.state, "event_service", None)
media_dir = Path(cfg.photobooth.media_dir)
# Récupère toutes les photos
all_photos = await pb.get_media_collection(limit=500)
photos = [p for p in all_photos if _is_image(p)]
# Enrichit chaque photo avec mtime depuis le disque
mtime_index = _build_mtime_index(media_dir)
for p in photos:
pid = _get_id(p)
p["photo_id"] = pid
stem = pid.rsplit(".", 1)[0] if "." in pid else pid
mt = mtime_index.get(pid) or mtime_index.get(stem) or 0.0
p["mtime"] = mt
p["date_iso"] = datetime.fromtimestamp(mt).strftime("%Y-%m-%d") if mt else ""
p["date_label"] = datetime.fromtimestamp(mt).strftime("%d/%m/%Y %H:%M") if mt else ""
# Filtre par événement
if event_slug and event_svc:
ev = await event_svc.get_event_by_slug(event_slug)
if ev:
t0 = ev["started_at"] or 0.0
t1 = ev["ended_at"] or datetime.now().timestamp()
photos = [p for p in photos if t0 <= p["mtime"] <= t1]
# Filtre par plage de dates
if date_from is not None:
photos = [p for p in photos if p["mtime"] >= date_from]
if date_to is not None:
photos = [p for p in photos if p["mtime"] <= date_to]
total = len(photos)
start = (page - 1) * limit
page_photos = photos[start:start + limit]
# Construit les URLs
for p in page_photos:
pid = p["photo_id"]
p["full_url"] = pb.media_url(pid)
p["thumb_url"] = pb.thumbnail_url(pid)
p["download_url"] = f"/api/gallery/download/{pid}"
# Croise avec la file d'impression en attente
try:
pending_map = await printer_svc.get_pending_by_photo_id()
for p in page_photos:
pid = p.get("photo_id", "")
requests = pending_map.get(pid, [])
p["print_requests"] = requests
p["print_pending"] = len([r for r in requests if r["status"] == "pending"])
p["print_printing"] = len([r for r in requests if r["status"] == "printing"])
except Exception as e:
logger.warning("Impossible de croiser avec print_queue: %s", e)
for p in page_photos:
p["print_requests"] = []
p["print_pending"] = 0
p["print_printing"] = 0
return {
"photos": page_photos,
"total": total,
"page": page,
"pages": max(1, (total + limit - 1) // limit),
}
# ── Demandes d'impression ─────────────────────────────────────────────────────
@router.post("/admin/api/gallery/print/{photo_id}")
async def admin_print_photo(
request: Request,
photo_id: str,
copies: int = Query(default=1, ge=1, le=3),
immediate: bool = Query(default=False, description="True = imprimer maintenant sans passer par la file"),
):
"""Ajoute une demande d'impression (ou imprime immédiatement si immediate=true).
En mode 'validation', la demande est mise en file — l'admin valide via /admin/print.
En mode 'direct' ou si immediate=true, l'impression est lancée tout de suite.
"""
if not _require_auth(request):
return JSONResponse({"error": "Non authentifié"}, status_code=401)
pb = request.app.state.photobooth_service
printer_svc = request.app.state.printer_service
led = request.app.state.led_service
ws = request.app.state.ws_manager
cfg = request.app.state.config
filename = _find_file(cfg.photobooth.media_dir, photo_id)
if not filename:
return JSONResponse({"error": f"Fichier introuvable pour {photo_id}"}, status_code=404)
thumb_url = pb.thumbnail_url(photo_id)
entry = await printer_svc.add_request(str(filename), thumb_url, copies)
should_print_now = (cfg.print.mode == "direct") or immediate
if should_print_now:
led.play("printing")
result = await printer_svc.execute_print(entry["id"], copies)
await ws.broadcast({"type": "print_result", "result": result, "photo_id": photo_id})
led.play("finished" if result["success"] else "error")
return {**result, "entry_id": entry["id"], "photo_id": photo_id}
# Mode validation : juste en file
await ws.broadcast({
"type": "print_request",
"entry": entry,
"photo_id": photo_id,
})
return {
"ok": True,
"queued": True,
"entry_id": entry["id"],
"photo_id": photo_id,
"mode": cfg.print.mode,
}
@router.delete("/admin/api/gallery/print/{photo_id}")
async def admin_cancel_print_requests(request: Request, photo_id: str):
"""Annule toutes les demandes d'impression en attente pour une photo."""
if not _require_auth(request):
return JSONResponse({"error": "Non authentifié"}, status_code=401)
printer_svc = request.app.state.printer_service
ws = request.app.state.ws_manager
count = await printer_svc.cancel_by_photo_id(photo_id)
if count:
await ws.broadcast({"type": "print_cancelled_for_photo", "photo_id": photo_id, "count": count})
return {"ok": True, "photo_id": photo_id, "cancelled": count}
@router.get("/admin/api/gallery/print-status")
async def admin_print_status(request: Request):
"""Retourne le dict {photo_id_stem: pending_count} pour toute la file.
Utilisé par la galerie pour mettre à jour les badges sans recharger les photos.
"""
if not _require_auth(request):
return JSONResponse({"error": "Non authentifié"}, status_code=401)
printer_svc = request.app.state.printer_service
pending_map = await printer_svc.get_pending_by_photo_id()
# Format compact : {stem: count} pour minimiser la taille de la réponse
return {
stem: len(entries)
for stem, entries in pending_map.items()
}
# ── Suppression ───────────────────────────────────────────────────────────────
@router.delete("/admin/api/gallery/{photo_id}")
async def admin_delete_photo(request: Request, photo_id: str):
"""Supprime une photo via l'API photobooth-app."""
if not _require_auth(request):
return JSONResponse({"error": "Non authentifié"}, status_code=401)
pb = request.app.state.photobooth_service
ws = request.app.state.ws_manager
ok = await pb.delete_media(photo_id)
if ok:
await ws.broadcast({"type": "photo_deleted", "photo_id": photo_id})
return {"ok": ok, "photo_id": photo_id}