Files
photobooth/backend/destinations.py
Jules 9ce8fc0e55 Gestion evenements + cadres par event + fix cadre paysage + dossier non-imprimees
- CRUD evenements : creer, activer, editer, supprimer dans panneau admin
- Cadres specifiques par evenement (aucun/disponible/defaut/impose)
- Fix cadre paysage deformé : rotation auto si orientation ne correspond pas
- Photos non imprimees vont dans sous-dossier "non_imprimees" (USB/FTP)
  au lieu d'etre ignorees ; booth recoit toujours toutes les photos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-23 00:16:35 +02:00

226 lines
7.2 KiB
Python

import ftplib
import logging
import shutil
import threading
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
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", {})
# Galerie live (booth) — toujours envoyer, imprimee ou non
booth = config.get("booth", {})
if booth.get("actif", False):
threading.Thread(
target=envoyer_booth,
args=(chemin_photo, booth),
daemon=True,
).start()
sous_dossier = None
if not dest.get("sauvegarder_tout", True) and not imprimee:
sous_dossier = "non_imprimees"
# Cle USB
if dest.get("cle_usb", False):
copier_usb(chemin_photo, dest.get("chemin_usb", "/media/usb"), sous_dossier)
# FTP
if dest.get("ftp", False):
envoyer_ftp(chemin_photo, dest, sous_dossier)
# 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, sous_dossier: str | None = None):
"""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"
if sous_dossier:
dossier_dest = dossier_dest / sous_dossier
dossier_dest.mkdir(parents=True, exist_ok=True)
try:
shutil.copy2(chemin_photo, dossier_dest / chemin_photo.name)
log.info(f"Photo copiee sur USB : {chemin_photo.name} ({sous_dossier or 'root'})")
return True
except OSError as e:
log.error(f"Erreur copie USB : {e}")
return False
def envoyer_ftp(chemin_photo: Path, config_dest: dict, sous_dossier: str | None = None):
"""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 sous_dossier:
chemin_distant = f"{chemin_distant}/{sous_dossier}"
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 envoyer_booth(chemin_photo: Path, config_booth: dict):
"""Envoie une photo vers la galerie live booth."""
url = config_booth.get("url", "").rstrip("/")
api_key = config_booth.get("api_key", "")
event_id = config_booth.get("event_id", "default")
if not url:
log.warning("Booth non configure (URL manquante)")
return False
try:
import mimetypes
boundary = "----BoothUpload"
filename = chemin_photo.name
with open(chemin_photo, "rb") as f:
file_data = f.read()
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="photo"; filename="{filename}"\r\n'
f"Content-Type: image/jpeg\r\n\r\n"
).encode() + file_data + f"\r\n--{boundary}--\r\n".encode()
req = Request(
f"{url}/api/{event_id}/upload",
data=body,
method="POST",
)
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
req.add_header("X-Api-Key", api_key)
with urlopen(req, timeout=10) as resp:
if resp.status == 200:
log.info(f"Photo envoyee au booth : {filename}")
return True
else:
log.error(f"Booth erreur HTTP {resp.status}")
return False
except (URLError, OSError) as e:
log.error(f"Erreur envoi booth : {e}")
return False
def recuperer_booth_password() -> dict:
"""Recupere le mot de passe et l'info de la session booth."""
config = charger_config()
booth = config.get("booth", {})
url = booth.get("url", "").rstrip("/")
api_key = booth.get("api_key", "")
event_id = booth.get("event_id", "default")
if not url:
return {"password": None}
try:
req = Request(f"{url}/api/{event_id}/info")
req.add_header("X-Api-Key", api_key)
with urlopen(req, timeout=5) as resp:
import json
data = json.loads(resp.read())
return data
except (URLError, OSError) as e:
log.error(f"Erreur recuperation booth info : {e}")
return {"password": None}
def detecter_usb() -> list[str]:
"""Detecte les cles USB montees (cherche dans /media, /mnt et /media/<user>/)."""
usb_trouvees = []
candidats = []
for racine in [Path("/media"), Path("/mnt")]:
if not racine.exists():
continue
try:
for niveau1 in racine.iterdir():
if not niveau1.is_dir():
continue
if niveau1.is_mount():
candidats.append(niveau1)
else:
# /media/<user>/ → descendre encore (udiskie)
try:
for niveau2 in niveau1.iterdir():
if niveau2.is_dir() and niveau2.is_mount():
candidats.append(niveau2)
except PermissionError:
pass
except PermissionError:
pass
for chemin in candidats:
# Exclure les partitions système
try:
result = __import__("subprocess").run(
["findmnt", "-no", "FSTYPE", str(chemin)],
capture_output=True, text=True, timeout=3
)
fstype = result.stdout.strip()
if fstype in ("vfat", "exfat", "ntfs", "ext4", "ext3", "hfsplus"):
usb_trouvees.append(str(chemin))
except Exception:
usb_trouvees.append(str(chemin))
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}})