"""Galerie publique -- accessible sans authentification.""" import asyncio import logging from datetime import datetime from pathlib import Path import httpx from fastapi import APIRouter, Request, Query from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi.templating import Jinja2Templates logger = logging.getLogger(__name__) router = APIRouter() _templates = Jinja2Templates(directory=Path(__file__).parent.parent.parent / "frontend" / "templates") @router.get("/", response_class=HTMLResponse) async def index(request: Request): return RedirectResponse(url="/gallery") @router.get("/gallery", response_class=HTMLResponse) async def gallery_page(request: Request): cfg = request.app.state.config if not cfg.gallery.public_enabled: return HTMLResponse("

Galerie desactivee

", status_code=403) return _templates.TemplateResponse(request, "public/gallery.html", {"config": cfg}) @router.get("/api/gallery/photos") async def api_gallery_photos( request: Request, page: int = Query(default=1, ge=1), limit: int = Query(default=24, ge=1, le=100), event_slug: str = Query(default=None, description="Filtrer par slug d'événement"), ): pb = request.app.state.photobooth_service cfg = request.app.state.config all_photos = await pb.get_media_collection(limit=500) photos = [p for p in all_photos if _is_image(p)] # Filtre par événement si demandé if event_slug: from pathlib import Path from datetime import datetime event_svc = getattr(request.app.state, "event_service", None) if event_svc: ev = await event_svc.get_event_by_slug(event_slug) if ev: started_at = ev["started_at"] or 0.0 ended_at = ev["ended_at"] or datetime.now().timestamp() media_dir = Path(cfg.photobooth.media_dir) # Construire un index mtime par nom de fichier mtime_index: dict[str, float] = {} if media_dir.exists(): for f in media_dir.iterdir(): mtime_index[f.name] = f.stat().st_mtime # Filtrer les photos par mtime def _in_event(p: dict) -> bool: pid = _get_id(p) mtime = mtime_index.get(pid, mtime_index.get(pid + ".jpg", 0.0)) return started_at <= mtime <= ended_at photos = [p for p in photos if _in_event(p)] total = len(photos) start = (page - 1) * limit end = start + limit page_photos = photos[start:end] for p in page_photos: pid = _get_id(p) p["full_url"] = pb.media_url(pid) p["thumb_url"] = pb.thumbnail_url(pid) p["download_url"] = f"/api/gallery/download/{pid}" return { "photos": page_photos, "total": total, "page": page, "pages": max(1, (total + limit - 1) // limit), "event_slug": event_slug, } @router.get("/api/gallery/download/{photo_id}") async def download_photo(request: Request, photo_id: str): """Proxy la photo full-res avec un nom de fichier lie a l'evenement en cours.""" pb = request.app.state.photobooth_service cfg = request.app.state.config event_svc = getattr(request.app.state, "event_service", None) img_url = pb.media_url(photo_id) slug = cfg.event.slug or "photomaton" date_str = datetime.now().strftime("%Y-%m-%d") filename = f"{slug}_By_LSDW_{date_str}.jpg" try: async with httpx.AsyncClient(timeout=30.0) as client: r = await client.get(img_url) r.raise_for_status() if event_svc: asyncio.create_task(event_svc.increment(slug, "downloads")) content_type = r.headers.get("content-type", "image/jpeg") return Response( content=r.content, media_type=content_type, headers={ "Content-Disposition": f'attachment; filename="{filename}"', "Content-Length": str(len(r.content)), "Cache-Control": "no-cache", }, ) except Exception as e: logger.warning("Download proxy echoue (%s), fallback redirect: %s", photo_id, e) return RedirectResponse(url=img_url) def _is_image(item: dict) -> bool: t = item.get("type", item.get("mediaitem_type", "image")) return str(t).lower() in ("image", "still", "photo") def _get_id(item: dict) -> str: return str(item.get("id", item.get("filename", item.get("uid", ""))))