"""Service de gestion des événements et statistiques. Chaque événement (soirée, mariage, fête...) a ses propres compteurs : - photos_taken : incrémenté par le webhook 'finished' de photobooth-app - print_requests : incrémenté à chaque demande d'impression dans la file - prints_done : incrémenté à chaque impression réussie - downloads : incrémenté à chaque téléchargement via /api/gallery/download/ """ from __future__ import annotations import logging import re import unicodedata from datetime import datetime from pathlib import Path import aiosqlite logger = logging.getLogger(__name__) def slugify(name: str) -> str: """Transforme un nom d'événement en slug ASCII sans accents ni espaces.""" # Normalise les caractères Unicode (enlève les accents) nfkd = unicodedata.normalize("NFD", name) ascii_str = "".join(c for c in nfkd if unicodedata.category(c) != "Mn") ascii_str = ascii_str.lower() # Garde lettres, chiffres, espaces, tirets ascii_str = re.sub(r"[^\w\s-]", "", ascii_str) # Remplace espaces/tirets multiples par un underscore ascii_str = re.sub(r"[\s_-]+", "_", ascii_str) return ascii_str.strip("_") or "evenement" _SCHEMA = """ CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, started_at REAL NOT NULL, ended_at REAL, photos_taken INTEGER NOT NULL DEFAULT 0, print_requests INTEGER NOT NULL DEFAULT 0, prints_done INTEGER NOT NULL DEFAULT 0, downloads INTEGER NOT NULL DEFAULT 0 ); """ _VALID_COUNTERS = {"photos_taken", "print_requests", "prints_done", "downloads"} class EventService: def __init__(self): self._db: aiosqlite.Connection | None = None async def init_db(self, db_path: Path): db_path.parent.mkdir(parents=True, exist_ok=True) self._db = await aiosqlite.connect(str(db_path)) self._db.row_factory = aiosqlite.Row await self._db.execute(_SCHEMA) await self._db.commit() logger.info("EventService initialisé : %s", db_path) async def ensure_event(self, slug: str, name: str, started_at: float): """Crée la ligne pour cet événement si elle n'existe pas encore.""" await self._db.execute( """INSERT INTO events (name, slug, started_at) VALUES (?, ?, ?) ON CONFLICT(slug) DO UPDATE SET name = excluded.name""", (name, slug, started_at), ) await self._db.commit() async def archive_event(self, slug: str): """Marque un événement comme terminé (ended_at = maintenant).""" await self._db.execute( "UPDATE events SET ended_at = ? WHERE slug = ? AND ended_at IS NULL", (datetime.now().timestamp(), slug), ) await self._db.commit() async def increment(self, slug: str, counter: str): """Incrémente un compteur de l'événement identifié par son slug. Crée automatiquement une ligne si elle n'existe pas. """ if counter not in _VALID_COUNTERS: logger.warning("Compteur inconnu : %s", counter) return # Upsert : insère si absent, sinon incrémente await self._db.execute( f"""INSERT INTO events (name, slug, started_at, {counter}) VALUES (?, ?, unixepoch(), 1) ON CONFLICT(slug) DO UPDATE SET {counter} = {counter} + 1""", (slug, slug), ) await self._db.commit() async def get_stats(self, slug: str) -> dict: """Retourne les stats pour un slug donné.""" async with self._db.execute( """SELECT photos_taken, print_requests, prints_done, downloads FROM events WHERE slug = ?""", (slug,), ) as cur: row = await cur.fetchone() if not row: return {"photos_taken": 0, "print_requests": 0, "prints_done": 0, "downloads": 0} return dict(row) async def get_history(self) -> list[dict]: """Retourne tous les événements triés du plus récent au plus ancien.""" async with self._db.execute( """SELECT id, name, slug, started_at, ended_at, photos_taken, print_requests, prints_done, downloads FROM events ORDER BY started_at DESC""" ) as cur: rows = await cur.fetchall() return [dict(r) for r in rows] async def close(self): if self._db: await self._db.close()