Backoffice complet : materiel, compteur, destinations, cadres, animations

- Onglet Materiel : choix appareil photo + imprimante
- Compteur avec limite configurable (399/400) affiche sur l'accueil
- Destinations multiples : memoire interne, cle USB, FTP, site web, email
- Choix sauvegarder toutes les photos ou seulement les imprimees
- Gestion des cadres : activation/desactivation + import
- 7 animations de compte a rebours : classique, explosion, rebond,
  rotation 3D, fondu, emoji fun, tremblement
- Preview des animations dans le backoffice
- Module destinations.py (copie USB, envoi FTP, compteur)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 23:03:30 +01:00
parent 3b3271a32f
commit d9de999d74
8 changed files with 1009 additions and 148 deletions

114
backend/destinations.py Normal file
View File

@@ -0,0 +1,114 @@
import ftplib
import logging
import shutil
from pathlib import Path
from backend.config import charger_config, mettre_a_jour_config
log = logging.getLogger("photobooth.destinations")
def distribuer_photo(chemin_photo: Path, imprimee: bool = False):
"""Copie la photo vers toutes les destinations activees."""
config = charger_config()
dest = config.get("destinations", {})
# Verifier si on sauvegarde tout ou seulement les imprimees
if not dest.get("sauvegarder_tout", True) and not imprimee:
log.info(f"Photo non imprimee, pas de distribution : {chemin_photo.name}")
return
# Cle USB
if dest.get("cle_usb", False):
copier_usb(chemin_photo, dest.get("chemin_usb", "/media/usb"))
# FTP
if dest.get("ftp", False):
envoyer_ftp(chemin_photo, dest)
# Incrementer le compteur
compteur = config.get("compteur", {})
if compteur.get("actif", False):
compteur["photos_prises"] = compteur.get("photos_prises", 0) + 1
mettre_a_jour_config({"compteur": compteur})
def copier_usb(chemin_photo: Path, chemin_usb: str):
"""Copie une photo sur la cle USB."""
dossier_usb = Path(chemin_usb)
if not dossier_usb.exists():
log.warning(f"Cle USB non trouvee : {chemin_usb}")
return False
dossier_dest = dossier_usb / "photobooth"
dossier_dest.mkdir(exist_ok=True)
try:
shutil.copy2(chemin_photo, dossier_dest / chemin_photo.name)
log.info(f"Photo copiee sur USB : {chemin_photo.name}")
return True
except OSError as e:
log.error(f"Erreur copie USB : {e}")
return False
def envoyer_ftp(chemin_photo: Path, config_dest: dict):
"""Envoie une photo par FTP."""
host = config_dest.get("ftp_host", "")
port = config_dest.get("ftp_port", 21)
user = config_dest.get("ftp_user", "")
password = config_dest.get("ftp_password", "")
chemin_distant = config_dest.get("ftp_chemin", "/photobooth")
if not host:
log.warning("FTP non configure")
return False
try:
with ftplib.FTP() as ftp:
ftp.connect(host, port, timeout=10)
ftp.login(user, password)
# Creer le dossier si necessaire
try:
ftp.mkd(chemin_distant)
except ftplib.error_perm:
pass
ftp.cwd(chemin_distant)
with open(chemin_photo, "rb") as f:
ftp.storbinary(f"STOR {chemin_photo.name}", f)
log.info(f"Photo envoyee par FTP : {chemin_photo.name}")
return True
except (ftplib.all_errors, OSError) as e:
log.error(f"Erreur FTP : {e}")
return False
def detecter_usb() -> list[str]:
"""Detecte les cles USB montees."""
chemins_possibles = [Path("/media"), Path("/mnt")]
usb_trouvees = []
for racine in chemins_possibles:
if racine.exists():
for sous_dossier in racine.iterdir():
if sous_dossier.is_mount() or (sous_dossier.is_dir() and any(sous_dossier.iterdir())):
usb_trouvees.append(str(sous_dossier))
return usb_trouvees
def compteur_restant() -> dict:
"""Retourne l'etat du compteur."""
config = charger_config()
compteur = config.get("compteur", {})
limite = compteur.get("limite", 400)
prises = compteur.get("photos_prises", 0)
return {
"actif": compteur.get("actif", False),
"limite": limite,
"photos_prises": prises,
"restantes": max(0, limite - prises),
}
def reset_compteur():
"""Remet le compteur a zero."""
mettre_a_jour_config({"compteur": {"photos_prises": 0}})

View File

@@ -17,7 +17,7 @@ from backend.camera import camera
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.collage import creer_strip, creer_collage
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur
from backend.printer import lister_imprimantes, imprimer
from backend.mailer import envoyer_photo
from backend.qrcode_gen import generer_qr, qr_galerie
@@ -73,10 +73,19 @@ async def api_config_update(modifications: dict):
@app.post("/api/capturer")
async def api_capturer():
# Verifier le compteur
etat = compteur_restant()
if etat["actif"] and etat["restantes"] <= 0:
return JSONResponse({"erreur": "Limite de photos atteinte"}, status_code=403)
chemin = camera.capturer()
if chemin is None:
return JSONResponse({"erreur": "Echec capture"}, status_code=500)
nom = chemin.name
# Distribuer vers les destinations
distribuer_photo(chemin, imprimee=False)
await diffuser_ws({"type": "photo_capturee", "nom": nom})
return {"nom": nom, "chemin": f"/data/photos/{nom}"}
@@ -206,6 +215,8 @@ async def api_imprimer(donnees: dict):
if not chemin.exists():
return JSONResponse({"erreur": "Photo introuvable"}, status_code=404)
ok = imprimer(chemin)
if ok:
distribuer_photo(chemin, imprimee=True)
return {"succes": ok}
@@ -278,6 +289,60 @@ async def api_upload_fond(fichier: UploadFile = File(...)):
return {"nom": fichier.filename}
# --- API Compteur ---
@app.get("/api/compteur")
async def api_compteur_etat():
return compteur_restant()
@app.post("/api/compteur/reset")
async def api_compteur_reset():
reset_compteur()
return compteur_restant()
# --- API Destinations ---
@app.get("/api/usb/detecter")
async def api_detecter_usb():
return detecter_usb()
# --- API Cadres actifs ---
@app.get("/api/cadres")
async def api_cadres():
config = charger_config()
tous = lister_overlays()
actifs = config.get("cadres", {}).get("actifs", [])
return {"tous": tous, "actifs": actifs}
@app.post("/api/cadres")
async def api_cadres_update(donnees: dict):
actifs = donnees.get("actifs", [])
mettre_a_jour_config({"cadres": {"actifs": actifs}})
return {"actifs": actifs}
# --- API Animations compte a rebours ---
ANIMATIONS_CAR = {
"classique": "Classique (chiffres simples)",
"explosion": "Explosion (chiffres qui eclatent)",
"rebond": "Rebond (chiffres qui rebondissent)",
"rotation": "Rotation 3D",
"fondu": "Fondu enchaine",
"emoji": "Emoji fun",
"shake": "Tremblement",
}
@app.get("/api/animations")
async def api_animations():
return ANIMATIONS_CAR
# --- API Systeme ---
@app.post("/api/systeme/redemarrer")