Impression Mitsubishi K60 + cadres + interface admin distante
- Intégration selphy_print (backend CUPS dyesub Mitsubishi CP-K60DW-S) - Formats impression : 15x20, 10x15, 10x15 2-strips avec cadre et orientation portrait/paysage - Cadres d'impression par format (strip/10x15/15x20) : upload, sélection, aperçu live - Cadres démo générés : pellicule noir/vintage/couleurs + bordures classique/doré/rose gold - Écran de choix de cadre avant capture (clic direct, aperçu grand format) - Filtres réduits à 3 (Couleur, N&B, Sépia) et appliqués sur tous les strips - Port 80 via authbind, route /admin avec redirection auto depuis poste distant - Autologin LightDM corrigé (pam-autologin-service) - Bouton évacuer bourrage papier dans admin - Fix : _gp_lock manquant dans Camera.__init__ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
104
backend/main.py
104
backend/main.py
@@ -9,12 +9,13 @@ from contextlib import asynccontextmanager
|
||||
|
||||
import cv2
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File
|
||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, UploadFile, File
|
||||
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from backend.config import (
|
||||
RACINE, DOSSIER_PHOTOS, DOSSIER_EXPORTS, DOSSIER_OVERLAYS, DOSSIER_FONDS, DOSSIER_ANIMATIONS,
|
||||
DOSSIER_CADRES, FORMATS_CADRES,
|
||||
charger_config, sauvegarder_config, mettre_a_jour_config,
|
||||
)
|
||||
from backend.camera import camera, GPHOTO2_DISPONIBLE
|
||||
@@ -23,7 +24,7 @@ try:
|
||||
except ImportError:
|
||||
gp = None
|
||||
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
|
||||
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, FILTRES
|
||||
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES
|
||||
from backend.collage import creer_strip, creer_collage, creer_impression_strip
|
||||
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
|
||||
from backend.printer import lister_imprimantes, imprimer
|
||||
@@ -150,8 +151,17 @@ app.mount("/data", StaticFiles(directory=str(RACINE / "data")), name="data")
|
||||
|
||||
# --- Pages ---
|
||||
|
||||
@app.get("/admin", include_in_schema=False)
|
||||
async def page_admin():
|
||||
return FileResponse(str(RACINE / "frontend" / "index.html"))
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def page_principale():
|
||||
async def page_principale(request: Request):
|
||||
from fastapi.responses import RedirectResponse
|
||||
host = request.client.host if request.client else ""
|
||||
if host not in ("127.0.0.1", "::1", "localhost"):
|
||||
return RedirectResponse(url="/admin", status_code=302)
|
||||
return FileResponse(str(RACINE / "frontend" / "index.html"))
|
||||
|
||||
|
||||
@@ -454,10 +464,16 @@ async def api_supprimer_fond(nom: str):
|
||||
@app.post("/api/strip")
|
||||
async def api_strip(donnees: dict):
|
||||
noms = donnees.get("photos", [])
|
||||
chemins = [DOSSIER_PHOTOS / n for n in noms]
|
||||
for c in chemins:
|
||||
source = donnees.get("source", "photos")
|
||||
dossier_src = DOSSIER_EXPORTS if source == "exports" else DOSSIER_PHOTOS
|
||||
chemins = []
|
||||
for n in noms:
|
||||
c = dossier_src / n
|
||||
if not c.exists():
|
||||
return JSONResponse({"erreur": f"Photo introuvable : {c.name}"}, status_code=404)
|
||||
c = DOSSIER_EXPORTS / n # fallback
|
||||
if not c.exists():
|
||||
return JSONResponse({"erreur": f"Photo introuvable : {n}"}, status_code=404)
|
||||
chemins.append(c)
|
||||
chemin_strip = creer_strip(chemins)
|
||||
# Creer aussi la page d'impression (2 bandes sur 10x15 paysage)
|
||||
chemin_print = creer_impression_strip(chemin_strip)
|
||||
@@ -489,6 +505,17 @@ async def api_imprimantes():
|
||||
return lister_imprimantes()
|
||||
|
||||
|
||||
@app.post("/api/imprimante/evacuer")
|
||||
async def api_evacuer_bourrage():
|
||||
"""Annule tous les jobs et réactive l'imprimante pour débloquer un bourrage."""
|
||||
import subprocess
|
||||
config = charger_config()
|
||||
nom = config.get("impression", {}).get("imprimante", "Mitsubishi")
|
||||
subprocess.run(["sudo", "cancel", "-a", nom], capture_output=True)
|
||||
subprocess.run(["sudo", "cupsenable", nom], capture_output=True)
|
||||
return {"succes": True, "message": f"Jobs annulés, {nom} réactivée"}
|
||||
|
||||
|
||||
@app.post("/api/imprimer")
|
||||
async def api_imprimer(donnees: dict):
|
||||
nom = donnees.get("photo", "")
|
||||
@@ -503,10 +530,11 @@ async def api_imprimer(donnees: dict):
|
||||
chemin = DOSSIER_PHOTOS / nom
|
||||
if not chemin.exists():
|
||||
return JSONResponse({"erreur": "Photo introuvable"}, status_code=404)
|
||||
ok = imprimer(chemin, copies=copies)
|
||||
if ok:
|
||||
cadre_override = donnees.get("cadre") # cadre choisi en session, prioritaire sur le config
|
||||
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override)
|
||||
if resultat.get("succes"):
|
||||
distribuer_photo(chemin, imprimee=True)
|
||||
return {"succes": ok}
|
||||
return resultat
|
||||
|
||||
|
||||
# --- API Booth (galerie live) ---
|
||||
@@ -620,7 +648,7 @@ async def api_detecter_usb():
|
||||
return detecter_usb()
|
||||
|
||||
|
||||
# --- API Cadres actifs ---
|
||||
# --- API Cadres actifs (overlays photo) ---
|
||||
|
||||
@app.get("/api/cadres")
|
||||
async def api_cadres():
|
||||
@@ -637,6 +665,60 @@ async def api_cadres_update(donnees: dict):
|
||||
return {"actifs": actifs}
|
||||
|
||||
|
||||
# --- API Cadres impression (par format) ---
|
||||
|
||||
@app.get("/api/cadres-impression/{format_papier}")
|
||||
async def api_cadres_impression_list(format_papier: str):
|
||||
if format_papier not in FORMATS_CADRES:
|
||||
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
|
||||
config = charger_config()
|
||||
actif = config.get("impression", {}).get("cadres_actifs", {}).get(format_papier)
|
||||
return {"format": format_papier, "disponibles": lister_cadres(format_papier), "actif": actif}
|
||||
|
||||
|
||||
@app.post("/api/cadres-impression/{format_papier}/actif")
|
||||
async def api_cadres_impression_set(format_papier: str, donnees: dict):
|
||||
if format_papier not in FORMATS_CADRES:
|
||||
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
|
||||
nom = donnees.get("nom") # None = désactiver
|
||||
config = charger_config()
|
||||
cadres_actifs = config.get("impression", {}).get("cadres_actifs", {})
|
||||
cadres_actifs[format_papier] = nom
|
||||
mettre_a_jour_config({"impression": {"cadres_actifs": cadres_actifs}})
|
||||
return {"format": format_papier, "actif": nom}
|
||||
|
||||
|
||||
@app.post("/api/upload/cadre/{format_papier}")
|
||||
async def api_upload_cadre(format_papier: str, fichier: UploadFile = File(...)):
|
||||
if format_papier not in FORMATS_CADRES:
|
||||
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
|
||||
if not fichier.filename.lower().endswith(".png"):
|
||||
return JSONResponse({"erreur": "Seuls les PNG sont acceptés"}, status_code=400)
|
||||
dossier = DOSSIER_CADRES / format_papier
|
||||
dossier.mkdir(parents=True, exist_ok=True)
|
||||
chemin = dossier / fichier.filename
|
||||
with open(chemin, "wb") as f:
|
||||
f.write(await fichier.read())
|
||||
return {"nom": fichier.filename, "format": format_papier}
|
||||
|
||||
|
||||
@app.delete("/api/cadres-impression/{format_papier}/{nom}")
|
||||
async def api_supprimer_cadre(format_papier: str, nom: str):
|
||||
if format_papier not in FORMATS_CADRES:
|
||||
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
|
||||
chemin = DOSSIER_CADRES / format_papier / nom
|
||||
if not chemin.exists() or chemin.parent != DOSSIER_CADRES / format_papier:
|
||||
return JSONResponse({"erreur": "Fichier introuvable"}, status_code=404)
|
||||
chemin.unlink()
|
||||
# Désactiver si c'était le cadre actif
|
||||
config = charger_config()
|
||||
cadres_actifs = config.get("impression", {}).get("cadres_actifs", {})
|
||||
if cadres_actifs.get(format_papier) == nom:
|
||||
cadres_actifs[format_papier] = None
|
||||
mettre_a_jour_config({"impression": {"cadres_actifs": cadres_actifs}})
|
||||
return {"succes": True}
|
||||
|
||||
|
||||
# --- API Animations compte a rebours ---
|
||||
|
||||
ANIMATIONS_CAR = {
|
||||
@@ -769,7 +851,7 @@ if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"backend.main:app",
|
||||
host=conf_srv.get("host", "0.0.0.0"),
|
||||
port=conf_srv.get("port", 8080),
|
||||
port=conf_srv.get("port", 80),
|
||||
reload=False,
|
||||
log_level="info",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user