all
This commit is contained in:
@@ -1,98 +1,15 @@
|
||||
"""API galerie admin — impression et suppression de photos."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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),
|
||||
):
|
||||
"""Liste des photos pour la galerie admin."""
|
||||
if not request.session.get("authenticated"):
|
||||
return JSONResponse({"error": "Non authentifié"}, status_code=401)
|
||||
|
||||
pb = request.app.state.photobooth_service
|
||||
all_photos = await pb.get_media_collection(limit=500)
|
||||
photos = [p for p in all_photos if _is_image(p)]
|
||||
|
||||
total = len(photos)
|
||||
start = (page - 1) * limit
|
||||
page_photos = photos[start:start + limit]
|
||||
|
||||
for p in page_photos:
|
||||
pid = _get_id(p)
|
||||
p["full_url"] = pb.media_url(pid)
|
||||
p["thumb_url"] = pb.thumbnail_url(pid)
|
||||
|
||||
return {"photos": page_photos, "total": total, "page": page,
|
||||
"pages": max(1, (total + limit - 1) // limit)}
|
||||
|
||||
|
||||
@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),
|
||||
):
|
||||
"""Impression directe depuis la galerie admin."""
|
||||
if not request.session.get("authenticated"):
|
||||
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
|
||||
|
||||
# Reconstruit le chemin du fichier depuis l'ID
|
||||
from pathlib import Path
|
||||
import re
|
||||
cfg = request.app.state.config
|
||||
|
||||
# photobooth-app identifiant → chemin fichier
|
||||
media_dir = cfg.photobooth.media_dir
|
||||
# Cherche le fichier correspondant
|
||||
filename = _find_file(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)
|
||||
|
||||
if cfg.print.mode == "direct":
|
||||
led.play("printing")
|
||||
result = await printer_svc.execute_print(entry["id"], copies)
|
||||
await ws.broadcast({"type": "print_result", "result": result})
|
||||
if result["success"]:
|
||||
led.play("finished")
|
||||
else:
|
||||
led.play("error")
|
||||
return result
|
||||
|
||||
await ws.broadcast({"type": "print_request", "entry": entry})
|
||||
return {"ok": True, "entry": entry, "mode": cfg.print.mode}
|
||||
|
||||
|
||||
@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 request.session.get("authenticated"):
|
||||
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}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _is_image(item: dict) -> bool:
|
||||
t = item.get("type", item.get("mediaitem_type", "image"))
|
||||
@@ -103,20 +20,179 @@ 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):
|
||||
def _find_file(media_dir: str, photo_id: str) -> Path | None:
|
||||
"""Cherche un fichier image correspondant à l'identifiant dans le répertoire media."""
|
||||
from pathlib import Path
|
||||
base = Path(media_dir)
|
||||
if not base.exists():
|
||||
return None
|
||||
|
||||
# L'ID peut être le stem du filename
|
||||
for ext in (".jpg", ".jpeg", ".png"):
|
||||
f = base / f"{photo_id}{ext}"
|
||||
if f.exists():
|
||||
return f
|
||||
# Cherche dans les sous-dossiers
|
||||
matches = list(base.rglob(f"{photo_id}{ext}"))
|
||||
if matches:
|
||||
return matches[0]
|
||||
return None
|
||||
|
||||
|
||||
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),
|
||||
):
|
||||
"""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
|
||||
|
||||
# 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)]
|
||||
|
||||
total = len(photos)
|
||||
start = (page - 1) * limit
|
||||
page_photos = photos[start:start + limit]
|
||||
|
||||
# Construit les URLs
|
||||
for p in page_photos:
|
||||
pid = _get_id(p)
|
||||
p["photo_id"] = pid
|
||||
p["full_url"] = pb.media_url(pid)
|
||||
p["thumb_url"] = pb.thumbnail_url(pid)
|
||||
|
||||
# Croise avec la file d'impression en attente (une seule requête SQLite)
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user