270 lines
11 KiB
Python
270 lines
11 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é.
|
|
"""
|
|
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}
|