"""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é. """ from pathlib import Path as _Path 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 ───────────────────────────────────────────────── media_dir = _Path(request.app.state.config.photobooth.media_dir) candidates = sorted(media_dir.glob(f"{photo_id}*")) if not candidates: return JSONResponse({"error": "Photo introuvable"}, status_code=404) filepath = str(candidates[0]) # ── Enqueue ─────────────────────────────────────────────────────────────── pb = request.app.state.photobooth_service 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}/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}