from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field, validator from typing import List, Optional from .utils import ( save_cameras, load_cameras, user_exists, _bucket_key, _list_prefix, _read_bucket_json, _write_bucket_json, BUCKET_ID, ) from huggingface_hub import batch_bucket_files, download_bucket_files import tempfile import os print(" CAMERA API LOADED ") router = APIRouter(prefix="/camera", tags=["Camera"]) # ================= MODELS ================= class CameraData(BaseModel): user_id: str = Field(..., min_length=1) camera_name: str = Field(..., min_length=1) camera_loc: Optional[List[float]] = None @validator("camera_loc") def validate_loc(cls, loc): if loc is None: return loc if len(loc) != 2: raise ValueError("camera_loc must be [lat, lon]") lat, lon = loc if not (-90 <= lat <= 90): raise ValueError("Latitude must be between -90 and 90") if not (-180 <= lon <= 180): raise ValueError("Longitude must be between -180 and 180") return loc class EditCameraData(BaseModel): user_id: str old_camera_name: str new_camera_name: str new_camera_loc: Optional[List[float]] = None # ← optional, won't error if missing @validator("new_camera_loc") def validate_loc(cls, loc): if loc is None: return loc if len(loc) != 2: raise ValueError("new_camera_loc must be [lat, lon]") lat, lon = loc if not (-90 <= lat <= 90): raise ValueError("Latitude must be between -90 and 90") if not (-180 <= lon <= 180): raise ValueError("Longitude must be between -180 and 180") return loc # ================= ROUTES ================= @router.get("/") def home(): return { "message": "Camera API is Running", "endpoints": [ "/camera/add_camera", "/camera/edit_camera", "/camera/delete_camera", "/camera/get_cameras?user_id=" ] } # ---------- GET CAMERAS ---------- @router.get("/get_cameras") def get_cameras(user_id: str = Query(...)): if not user_exists(user_id): raise HTTPException(status_code=404, detail="User not found") cameras = load_cameras(user_id) return {"success": True, "user_id": user_id, "cameras": cameras, "count": len(cameras)} # ---------- ADD CAMERA ---------- @router.post("/add_camera") def add_camera(data: CameraData): cameras = load_cameras(data.user_id) if len(cameras) >= 2: raise HTTPException(status_code=400, detail="Only 2 cameras allowed") for cam in cameras: if cam["camera_name"].lower() == data.camera_name.lower(): raise HTTPException(status_code=400, detail="Camera already exists") cameras.append({"camera_name": data.camera_name, "camera_loc": data.camera_loc}) save_cameras(data.user_id, cameras) return {"success": True, "camera": data.camera_name} # ---------- EDIT CAMERA ---------- @router.put("/edit_camera") def edit_camera(data: EditCameraData): if not user_exists(data.user_id): raise HTTPException(status_code=404, detail="User not found") cameras = load_cameras(data.user_id) # Only block duplicate name if name is actually changing name_changing = data.new_camera_name.lower() != data.old_camera_name.lower() if name_changing: if any(cam["camera_name"].lower() == data.new_camera_name.lower() for cam in cameras): raise HTTPException( status_code=400, detail=f"Camera name '{data.new_camera_name}' already exists" ) camera_found = False for cam in cameras: if cam["camera_name"].lower() == data.old_camera_name.lower(): old_name = cam["camera_name"] # Apply updates cam["camera_name"] = data.new_camera_name if data.new_camera_loc is not None: # only update if provided cam["camera_loc"] = data.new_camera_loc camera_found = True # ── Rename bucket files only if name changed ────────── if name_changing: old_prefix = _bucket_key(data.user_id, old_name) new_prefix = _bucket_key(data.user_id, data.new_camera_name) old_files = _list_prefix(old_prefix) for item in old_files: old_key = item.path new_key = old_key.replace(old_prefix, new_prefix, 1) # Rename detection JSON filename inside the key if f"{old_name}_detections.json" in new_key: new_key = new_key.replace( f"{old_name}_detections.json", f"{data.new_camera_name}_detections.json" ) try: if old_key.endswith(".json"): # JSON: read and re-write content = _read_bucket_json(old_key) if content is not None: _write_bucket_json(new_key, content) else: # Binary (images): download → re-upload with tempfile.NamedTemporaryFile(delete=False) as tf: tmp_path = tf.name download_bucket_files( BUCKET_ID, files=[(old_key, tmp_path)], ) with open(tmp_path, "rb") as f: raw = f.read() os.unlink(tmp_path) batch_bucket_files( BUCKET_ID, add=[(raw, new_key)], ) # Delete old key after successful copy batch_bucket_files( BUCKET_ID, delete=[old_key], ) except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to move bucket file '{old_key}': {str(e)}" ) save_cameras(data.user_id, cameras) return {"success": True, "updated": cam} if not camera_found: raise HTTPException(status_code=404, detail="Camera not found") # ---------- DELETE CAMERA ---------- @router.delete("/delete_camera") def delete_camera(user_id: str = Query(...), camera_name: str = Query(...)): if not user_exists(user_id): raise HTTPException(status_code=404, detail="User not found") cameras = load_cameras(user_id) new_list = [c for c in cameras if c["camera_name"].lower() != camera_name.lower()] if len(new_list) == len(cameras): raise HTTPException(status_code=404, detail="Camera not found") # Delete all bucket files under this camera prefix cam_prefix = _bucket_key(user_id, camera_name) cam_files = _list_prefix(cam_prefix) if cam_files: keys_to_delete = [item.path for item in cam_files] try: batch_bucket_files( BUCKET_ID, delete=keys_to_delete, ) except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to delete camera files from bucket: {str(e)}" ) save_cameras(user_id, new_list) return {"success": True, "deleted": camera_name}