Files
photoBooth/backend/api/print_api.py
T
admin 19557b20fb
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
fix: timing flash + couleur idle LED + diagnostic auto-brightness
2026-07-20 19:06:16 +02:00

284 lines
12 KiB
Python

"""API gestion de la file d'attente d'impression."""
import asyncio
import logging
from pathlib import Path
from fastapi import APIRouter, Request, Query, Body
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/print/request")
async def print_request(
request: Request,
filename: str = Query(..., description="Chemin absolu du fichier à imprimer"),
copies: int = Query(default=1, ge=1, le=3),
):
"""
Reçoit la demande d'impression depuis photobooth-app
(appelé par le share_command 'Demande d'impression').
"""
printer_svc = request.app.state.printer_service
pb = request.app.state.photobooth_service
led = request.app.state.led_service
ws = request.app.state.ws_manager
# Construit l'URL de miniature depuis le filename
stem = Path(filename).stem
thumb_url = pb.thumbnail_url(stem)
entry = await printer_svc.add_request(filename, thumb_url, copies)
await ws.broadcast({"type": "print_request", "entry": entry})
# Compteur statistiques événement
event_svc = getattr(request.app.state, "event_service", None)
if event_svc:
slug = request.app.state.config.event.slug
asyncio.create_task(event_svc.increment(slug, "print_requests"))
# Feedback LED si impression directe
if request.app.state.config.print.mode == "direct":
led.play("printing")
logger.info("Demande impression reçue: %s", filename)
return {"ok": True, "id": entry["id"], "mode": request.app.state.config.print.mode}
@router.get("/print/queue")
async def get_queue(
request: Request,
status: str | None = Query(default=None),
):
"""Retourne la file d'attente d'impression."""
printer_svc = request.app.state.printer_service
queue = await printer_svc.get_queue(status)
printers = await printer_svc.get_printers_status()
return {"queue": queue, "printers": printers}
@router.post("/print/execute/{entry_id}")
async def execute_print(
request: Request,
entry_id: str,
copies: int = Query(default=1, ge=1, le=3),
):
"""Lance l'impression d'une entrée en attente (validation admin)."""
printer_svc = request.app.state.printer_service
led = request.app.state.led_service
ws = request.app.state.ws_manager
led.play("printing")
result = await printer_svc.execute_print(entry_id, copies)
await ws.broadcast({"type": "print_result", "entry_id": entry_id, "result": result})
if result["success"]:
led.play("finished")
event_svc = getattr(request.app.state, "event_service", None)
if event_svc:
slug = request.app.state.config.event.slug
asyncio.create_task(event_svc.increment(slug, "prints_done"))
else:
led.play("error")
return result
@router.post("/print/cancel/{entry_id}")
async def cancel_print(request: Request, entry_id: str):
"""Annule une demande en attente."""
printer_svc = request.app.state.printer_service
ws = request.app.state.ws_manager
ok = await printer_svc.cancel(entry_id)
if ok:
await ws.broadcast({"type": "print_cancelled", "entry_id": entry_id})
return {"ok": ok}
@router.post("/print/cups/cancel/{printer_name}")
async def cancel_cups_jobs(request: Request, printer_name: str):
"""Annule tous les jobs CUPS d'une imprimante."""
printer_svc = request.app.state.printer_service
ok = await printer_svc.cancel_cups_jobs(printer_name)
return {"ok": ok, "printer": printer_name}
@router.get("/print/printers")
async def get_printers(request: Request):
"""Statut des imprimantes CUPS."""
printer_svc = request.app.state.printer_service
return await printer_svc.get_printers_status()
@router.post("/print/mode")
async def set_print_mode(request: Request, mode: str = Query(...)):
"""Change le mode d'impression (direct | validation | gallery)."""
if mode not in ("direct", "validation", "gallery"):
return JSONResponse({"error": "Mode invalide"}, status_code=400)
config_svc = request.app.state.config_service
config_svc.save_print_mode(mode)
request.app.state.config.print.mode = mode
return {"ok": True, "mode": mode}
@router.post("/print/user-print-config")
async def set_user_print_config(request: Request, body: dict = Body(...)):
"""Configure l'impression depuis la galerie utilisateur."""
enabled = bool(body.get("user_print_enabled", False))
quota = max(0, int(body.get("user_print_quota", 0)))
max_copies = max(1, min(10, int(body.get("user_print_max_copies", 1))))
config_svc = request.app.state.config_service
config_svc.save_user_print_config(enabled, quota, max_copies)
logger.info("User print config: enabled=%s quota=%d max_copies=%d", enabled, quota, max_copies)
return {"ok": True, "user_print_enabled": enabled, "user_print_quota": quota, "user_print_max_copies": max_copies}
@router.get("/print/user-print-config")
async def get_user_print_config(request: Request):
"""Retourne la config impression galerie utilisateur."""
cfg = request.app.state.config.print
return {
"user_print_enabled": cfg.user_print_enabled,
"user_print_quota": cfg.user_print_quota,
"user_print_max_copies": cfg.user_print_max_copies,
}
@router.get("/print/user-print-status")
async def get_user_print_status(request: Request):
"""Statut d'impression de la session courante (quota restant, config active)."""
cfg = request.app.state.config.print
count = request.session.get("gallery_print_count", 0)
remaining = (cfg.user_print_quota - count) if cfg.user_print_quota > 0 else -1
return {
"user_print_enabled": cfg.user_print_enabled,
"user_print_quota": cfg.user_print_quota,
"user_print_max_copies": cfg.user_print_max_copies,
"session_print_count": count,
"remaining_quota": remaining, # -1 = illimité
}
@router.post("/print/request-gallery")
async def gallery_print_request(request: Request, body: dict = Body(...)):
"""Demande d'impression depuis la galerie publique.
Vérifie que l'impression est activée et que le quota session n'est pas dépassé.
"""
cfg_print = request.app.state.config.print
# ── Activation ────────────────────────────────────────────────────────────
if not cfg_print.user_print_enabled:
return JSONResponse({"error": "L'impression est désactivée"}, status_code=403)
photo_id = str(body.get("photo_id", "")).strip()
if not photo_id:
return JSONResponse({"error": "photo_id requis"}, status_code=400)
copies = max(1, min(cfg_print.user_print_max_copies, int(body.get("copies", 1))))
# ── Quota session ─────────────────────────────────────────────────────────
if cfg_print.user_print_quota > 0:
count = request.session.get("gallery_print_count", 0)
if count >= cfg_print.user_print_quota:
return JSONResponse({
"error": f"Quota atteint ({cfg_print.user_print_quota} impression(s) maximum par session)",
"quota_exceeded": True,
}, status_code=429)
# ── Résolution du fichier via l'API photobooth-app ───────────────────────
# On passe par get_media_item() comme le fait admin_print_photo — le champ
# "processed" contient le chemin réel sur le disque (ex: media/processed_full/xxx.jpg).
# Un simple glob sur photo_id ne marche pas car l'id interne ≠ nom de fichier.
pb = request.app.state.photobooth_service
item = await pb.get_media_item(photo_id)
if not item:
return JSONResponse({"error": "Photo introuvable"}, status_code=404)
file_path = pb.media_file_path(item)
if not file_path or not file_path.exists():
logger.error("Fichier manquant pour %s: %s", photo_id, file_path)
return JSONResponse({"error": "Fichier photo introuvable sur le disque"}, status_code=404)
filepath = str(file_path)
# ── Enqueue ───────────────────────────────────────────────────────────────
printer_svc = request.app.state.printer_service
ws = request.app.state.ws_manager
led = request.app.state.led_service
thumb_url = pb.thumbnail_url(photo_id)
entry = await printer_svc.add_request(filepath, thumb_url, copies)
await ws.broadcast({"type": "print_request", "entry": entry})
# ── Incrémenter compteur session ──────────────────────────────────────────
new_count = request.session.get("gallery_print_count", 0) + 1
request.session["gallery_print_count"] = new_count
remaining = (cfg_print.user_print_quota - new_count) if cfg_print.user_print_quota > 0 else -1
# ── Stats événement ───────────────────────────────────────────────────────
event_svc = getattr(request.app.state, "event_service", None)
if event_svc:
slug = request.app.state.config.event.slug
asyncio.create_task(event_svc.increment(slug, "print_requests"))
# ── LED si mode direct ────────────────────────────────────────────────────
if request.app.state.config.print.mode == "direct":
led.play("printing")
logger.info("Impression galerie: photo_id=%s copies=%d session_count=%d", photo_id, copies, new_count)
return {
"ok": True,
"id": entry["id"],
"mode": request.app.state.config.print.mode,
"remaining_quota": remaining,
}
# ── Gestion avancée des imprimantes CUPS ──────────────────────────────────────
@router.post("/print/printers/{printer_name}/set-excluded")
async def set_printer_excluded(request: Request, printer_name: str, excluded: bool = Query(...)):
"""Exclut (ou réintègre) une imprimante du load-balancing JH-Photomaton.
N'agit pas sur CUPS — sert uniquement à ignorer une imprimante temporairement hors ligne."""
config_svc = request.app.state.config_service
config_svc.save_printer_excluded(printer_name, excluded)
logger.info("Imprimante %s : excluded=%s", printer_name, excluded)
return {"ok": True, "printer": printer_name, "excluded": excluded}
@router.post("/print/printers/{printer_name}/enable")
async def printer_enable(request: Request, printer_name: str):
"""Active une imprimante CUPS (cupsenable + cupsaccept)."""
ok = await request.app.state.printer_service.enable_printer(printer_name)
return {"ok": ok, "printer": printer_name, "action": "enable"}
@router.post("/print/printers/{printer_name}/disable")
async def printer_disable(request: Request, printer_name: str):
"""Désactive une imprimante CUPS (cupsdisable)."""
ok = await request.app.state.printer_service.disable_printer(printer_name)
return {"ok": ok, "printer": printer_name, "action": "disable"}
@router.post("/print/printers/{printer_name}/reject")
async def printer_reject(request: Request, printer_name: str):
"""Refuse les nouveaux jobs sans stopper l'impression en cours."""
ok = await request.app.state.printer_service.reject_jobs(printer_name)
return {"ok": ok, "printer": printer_name, "action": "reject"}
@router.get("/print/printers/{printer_name}/jobs")
async def printer_jobs(request: Request, printer_name: str):
"""Liste les jobs CUPS en cours pour une imprimante."""
jobs = await request.app.state.printer_service.get_cups_jobs(printer_name)
return {"printer": printer_name, "jobs": jobs}
@router.delete("/print/printers/jobs/{job_id}")
async def cancel_one_job(request: Request, job_id: str):
"""Annule un job CUPS précis par son ID."""
ok = await request.app.state.printer_service.cancel_cups_job(job_id)
return {"ok": ok, "job_id": job_id}