""" Library API router for stats and scanning """ from fastapi import APIRouter, Depends from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import Photo router = APIRouter() @router.get("/stats") async def get_library_stats(db: AsyncSession = Depends(get_db)): """Get library statistics + per-section counts. Each section count matches the filter the sidebar applies when you click it, so the sidebar badges and the timeline below them stay in sync. - all_photos: non-discarded photos + videos (matches the All Photos section's default filter) - rated: non-discarded with rating >= 1 - duplicates: non-discarded with is_duplicate = true - discarded: is_discarded = true - total_size: raw bytes across every row, including discarded """ not_discarded = Photo.is_discarded.is_(False) all_photos_count = ( await db.execute(select(func.count(Photo.id)).where(not_discarded)) ).scalar() or 0 rated_count = ( await db.execute( select(func.count(Photo.id)).where(not_discarded, Photo.rating >= 1) ) ).scalar() or 0 duplicates_count = ( await db.execute( select(func.count(Photo.id)).where( not_discarded, Photo.is_duplicate.is_(True) ) ) ).scalar() or 0 discarded_count = ( await db.execute( select(func.count(Photo.id)).where(Photo.is_discarded.is_(True)) ) ).scalar() or 0 # Legacy split (kept for the existing /stats consumers). photo_count = ( await db.execute( select(func.count(Photo.id)).where( Photo.media_type.in_(['photo', 'heic', 'raw']) ) ) ).scalar() or 0 video_count = ( await db.execute( select(func.count(Photo.id)).where(Photo.media_type == 'video') ) ).scalar() or 0 size = (await db.execute(select(func.sum(Photo.file_size)))).scalar() or 0 return { "all_photos": all_photos_count, "rated": rated_count, "duplicates": duplicates_count, "discarded": discarded_count, "total_photos": photo_count, "total_videos": video_count, "total_size": size, "total_size_gb": round(size / (1024**3), 2) if size else 0, } @router.post("/scan") async def trigger_scan(): """Trigger full library re-scan""" from app.tasks.scan import scan_all_source_roots scan_all_source_roots.delay() return {"status": "success", "message": "Library scan started"} @router.get("/scan/status") async def get_scan_status(db: AsyncSession = Depends(get_db)): """Get current scan status""" import redis from app.config import settings # Connect to Redis to get scan status r = redis.Redis.from_url(settings.redis_url) # Get scan status from Redis (set by worker tasks) is_scanning = r.get('scan:active') == b'true' current_folder = r.get('scan:current_folder') processed_files = int(r.get('scan:processed_files') or 0) total_files = int(r.get('scan:total_files') or 0) errors = r.lrange('scan:errors', 0, -1) return { "is_scanning": is_scanning, "current_folder": current_folder.decode() if current_folder else None, "processed_files": processed_files, "total_files": total_files, "errors": [e.decode() for e in errors] if errors else [] }