"""Analysis job endpoints.""" from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.orm import Session from pydantic import BaseModel from typing import Dict, Optional from uuid import uuid4 import asyncio from ...models.database import get_db from ...models import crud from ...core.analyzer import AudioAnalyzer from ...utils.logging import get_logger from ...utils.validators import validate_directory_path router = APIRouter() logger = get_logger(__name__) # In-memory job storage (in production, use Redis) jobs: Dict[str, dict] = {} class AnalyzeFolderRequest(BaseModel): """Request to analyze a folder.""" path: str recursive: bool = True class JobStatus(BaseModel): """Analysis job status.""" job_id: str status: str # pending, running, completed, failed progress: int total: int current_file: Optional[str] = None errors: list = [] def analyze_folder_task(job_id: str, path: str, recursive: bool, db_url: str): """Background task to analyze folder. Args: job_id: Job UUID path: Directory path recursive: Scan recursively db_url: Database URL for new session """ from ...models.database import SessionLocal try: logger.info(f"Starting analysis job {job_id} for {path}") # Update job status jobs[job_id]["status"] = "running" # Create analyzer analyzer = AudioAnalyzer() # Progress callback def progress_callback(current: int, total: int, filename: str): jobs[job_id]["progress"] = current jobs[job_id]["total"] = total jobs[job_id]["current_file"] = filename # Analyze folder results = analyzer.analyze_folder( path=path, recursive=recursive, progress_callback=progress_callback, ) # Save to database db = SessionLocal() try: saved_count = 0 for analysis in results: try: crud.upsert_track(db, analysis) saved_count += 1 except Exception as e: logger.error(f"Failed to save track {analysis.filename}: {e}") jobs[job_id]["errors"].append({ "file": analysis.filename, "error": str(e) }) logger.info(f"Job {job_id} completed: {saved_count}/{len(results)} tracks saved") # Update job status jobs[job_id]["status"] = "completed" jobs[job_id]["progress"] = len(results) jobs[job_id]["total"] = len(results) jobs[job_id]["current_file"] = None jobs[job_id]["saved_count"] = saved_count finally: db.close() except Exception as e: logger.error(f"Job {job_id} failed: {e}") jobs[job_id]["status"] = "failed" jobs[job_id]["errors"].append({ "error": str(e) }) @router.post("/folder") async def analyze_folder( request: AnalyzeFolderRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db), ): """Start folder analysis job. Args: request: Folder analysis request background_tasks: FastAPI background tasks db: Database session Returns: Job ID for status tracking Raises: HTTPException: 400 if path is invalid """ # Validate path validated_path = validate_directory_path(request.path) if not validated_path: raise HTTPException( status_code=400, detail=f"Invalid or inaccessible directory: {request.path}" ) # Create job job_id = str(uuid4()) jobs[job_id] = { "job_id": job_id, "status": "pending", "progress": 0, "total": 0, "current_file": None, "errors": [], "path": validated_path, "recursive": request.recursive, } # Get database URL for background task from ...utils.config import settings # Start background task background_tasks.add_task( analyze_folder_task, job_id, validated_path, request.recursive, settings.DATABASE_URL, ) logger.info(f"Created analysis job {job_id} for {validated_path}") return { "job_id": job_id, "message": "Analysis job started", "path": validated_path, "recursive": request.recursive, } @router.get("/status/{job_id}") async def get_job_status(job_id: str): """Get analysis job status. Args: job_id: Job UUID Returns: Job status Raises: HTTPException: 404 if job not found """ if job_id not in jobs: raise HTTPException(status_code=404, detail="Job not found") job_data = jobs[job_id] return { "job_id": job_data["job_id"], "status": job_data["status"], "progress": job_data["progress"], "total": job_data["total"], "current_file": job_data.get("current_file"), "errors": job_data.get("errors", []), "saved_count": job_data.get("saved_count"), } @router.delete("/job/{job_id}") async def delete_job(job_id: str): """Delete job from memory. Args: job_id: Job UUID Returns: Success message Raises: HTTPException: 404 if job not found """ if job_id not in jobs: raise HTTPException(status_code=404, detail="Job not found") del jobs[job_id] return {"message": "Job deleted", "job_id": job_id}