""" 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""" # Count total photos total_photos = await db.execute( select(func.count(Photo.id)).where(Photo.media_type.in_(['photo', 'heic', 'raw'])) ) photo_count = total_photos.scalar() # Count total videos total_videos = await db.execute( select(func.count(Photo.id)).where(Photo.media_type == 'video') ) video_count = total_videos.scalar() # Calculate total size total_size = await db.execute( select(func.sum(Photo.file_size)) ) size = total_size.scalar() or 0 return { "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("/date_buckets") async def get_date_buckets(db: AsyncSession = Depends(get_db)): """Aggregate non-discarded photos by year and month based on taken_at. Used by the LeftSidebar 'By Date' subtree to render a year-then-month drilldown without fetching every photo. Returns: [{ year: 2026, count: 5, months: [{ month: 4, count: 5 }] }] """ year_expr = func.strftime('%Y', Photo.taken_at) month_expr = func.strftime('%m', Photo.taken_at) result = await db.execute( select( year_expr.label('year'), month_expr.label('month'), func.count(Photo.id).label('count'), ) .where( Photo.is_discarded == False, # noqa: E712 Photo.taken_at.is_not(None), ) .group_by(year_expr, month_expr) .order_by(year_expr.desc(), month_expr.desc()) ) rows = result.all() years: dict[int, dict] = {} for year_str, month_str, count in rows: if not year_str or not month_str: continue try: year = int(year_str) month = int(month_str) except ValueError: continue bucket = years.setdefault( year, {'year': year, 'count': 0, 'months': []} ) bucket['count'] += int(count) bucket['months'].append({'month': month, 'count': int(count)}) # Already ordered by the SQL ORDER BY (newest first), preserve insertion. return list(years.values()) @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 [] }