#!/usr/bin/env python3 """ FastAPI routes for NFOGuard """ import json from datetime import datetime, timezone from pathlib import Path from typing import Dict, Any, Optional from fastapi import FastAPI, HTTPException, BackgroundTasks, Request from fastapi.responses import JSONResponse # Import models from .models import SonarrWebhook, RadarrWebhook, HealthResponse, TVSeasonRequest, TVEpisodeRequest # Import core utilities from core.logging import _log from core.database import NFOGuardDatabase async def _read_payload(request: Request) -> Optional[Dict[str, Any]]: """Read and validate JSON payload from webhook""" try: content_type = request.headers.get("content-type", "") if "application/json" not in content_type.lower(): _log("WARNING", f"Invalid content type: {content_type}") return None body = await request.body() if not body: _log("WARNING", "Empty request body") return None payload = json.loads(body.decode('utf-8')) _log("DEBUG", f"Raw payload: {json.dumps(payload, indent=2)}") return payload except json.JSONDecodeError as e: _log("ERROR", f"Invalid JSON payload: {e}") return None except Exception as e: _log("ERROR", f"Error reading payload: {e}") return None def register_routes( app: FastAPI, tv_processor, movie_processor, batcher, db: NFOGuardDatabase, start_time: datetime, version: str ): """Register all routes with the FastAPI app""" @app.post("/webhook/sonarr") async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks): """Handle Sonarr webhooks""" try: payload = await _read_payload(request) if not payload: raise HTTPException(status_code=422, detail="Empty Sonarr payload") webhook = SonarrWebhook(**payload) _log("INFO", f"Received Sonarr webhook: {webhook.eventType}") if webhook.eventType not in ["Download", "Upgrade", "Rename"]: return {"status": "ignored", "reason": f"Event type {webhook.eventType} not processed"} if not webhook.series: _log("WARNING", "No series data in Sonarr webhook") return {"status": "error", "message": "No series data"} # Process the webhook using the batcher await batcher.queue_sonarr_webhook(payload) return {"status": "queued", "message": f"Sonarr {webhook.eventType} webhook queued for processing"} except Exception as e: _log("ERROR", f"Sonarr webhook error: {e}") raise HTTPException(status_code=500, detail=f"Webhook processing failed: {str(e)}") @app.post("/webhook/radarr") async def radarr_webhook(request: Request, background_tasks: BackgroundTasks): """Handle Radarr webhooks""" try: payload = await _read_payload(request) if not payload: raise HTTPException(status_code=422, detail="Empty Radarr payload") webhook = RadarrWebhook(**payload) _log("INFO", f"Received Radarr webhook: {webhook.eventType}") if webhook.eventType not in ["Download", "Upgrade", "Rename"]: return {"status": "ignored", "reason": f"Event type {webhook.eventType} not processed"} if not webhook.movie: _log("WARNING", "No movie data in Radarr webhook") return {"status": "error", "message": "No movie data"} # Process the webhook using the batcher await batcher.queue_radarr_webhook(payload) return {"status": "queued", "message": f"Radarr {webhook.eventType} webhook queued for processing"} except Exception as e: _log("ERROR", f"Radarr webhook error: {e}") raise HTTPException(status_code=500, detail=f"Webhook processing failed: {str(e)}") @app.get("/health") async def health() -> HealthResponse: """Health check endpoint with database status""" uptime = datetime.now(timezone.utc) - start_time # Check NFOGuard database try: with db.get_connection() as conn: conn.execute("SELECT 1").fetchone() db_status = "healthy" except Exception as e: db_status = f"error: {e}" # Check Radarr database if available radarr_db_health = None overall_status = "healthy" if db_status == "healthy" else "degraded" # Get Radarr client with database access from movie processor try: if hasattr(movie_processor, 'radarr') and movie_processor.radarr: if hasattr(movie_processor.radarr, 'db_client') and movie_processor.radarr.db_client: radarr_test = movie_processor.radarr.db_client.test_connection() radarr_db_health = { "radarr_db_available": radarr_test["success"], "radarr_db_path": radarr_test.get("db_path"), "radarr_db_error": radarr_test.get("error") } if not radarr_test["success"]: overall_status = "degraded" except Exception as e: _log("DEBUG", f"Radarr database health check failed: {e}") return HealthResponse( status=overall_status, version=version, uptime=str(uptime).split('.')[0], # Remove microseconds database_status=db_status, radarr_database=radarr_db_health ) @app.get("/stats") async def get_stats(): """Get processing statistics""" return { "message": "Stats endpoint - implementation needed", "uptime": str(datetime.now(timezone.utc) - start_time).split('.')[0] } @app.get("/batch/status") async def batch_status(): """Get current batch processing status""" return { "pending_items": batcher.get_pending_count(), "processing_items": batcher.get_processing_count(), "total_queued": batcher.get_pending_count() + batcher.get_processing_count() } @app.post("/manual/scan") async def manual_scan(background_tasks: BackgroundTasks): """Trigger manual library scan""" try: _log("INFO", "Manual scan triggered via API") # Add background task to scan all libraries def scan_libraries(): total_processed = 0 # Scan TV libraries for tv_path in config.tv_paths: if tv_path.exists(): _log("INFO", f"Scanning TV library: {tv_path}") for series_dir in tv_path.iterdir(): if series_dir.is_dir() and "[imdb-" in series_dir.name: try: tv_processor.process_series(series_dir) total_processed += 1 except Exception as e: _log("ERROR", f"Error processing series {series_dir.name}: {e}") # Scan Movie libraries for movie_path in config.movie_paths: if movie_path.exists(): _log("INFO", f"Scanning movie library: {movie_path}") for movie_dir in movie_path.iterdir(): if movie_dir.is_dir(): try: movie_processor.process_movie(movie_dir) total_processed += 1 except Exception as e: _log("ERROR", f"Error processing movie {movie_dir.name}: {e}") _log("INFO", f"Manual scan completed: {total_processed} items processed") background_tasks.add_task(scan_libraries) return {"status": "started", "message": "Manual scan initiated", "note": "Scan running in background - check logs for progress"} except Exception as e: _log("ERROR", f"Manual scan failed: {e}") raise HTTPException(status_code=500, detail=f"Manual scan failed: {str(e)}") @app.post("/tv/scan-season") async def scan_tv_season(request: TVSeasonRequest, background_tasks: BackgroundTasks): """Scan specific TV season""" try: _log("INFO", f"TV season scan requested: {request.series_path}/{request.season_name}") return {"status": "started", "message": f"Scanning season {request.season_name}"} except Exception as e: _log("ERROR", f"TV season scan failed: {e}") raise HTTPException(status_code=500, detail=f"Season scan failed: {str(e)}") @app.post("/tv/scan-episode") async def scan_tv_episode(request: TVEpisodeRequest, background_tasks: BackgroundTasks): """Scan specific TV episode""" try: _log("INFO", f"TV episode scan requested: {request.series_path}/{request.season_name}/{request.episode_name}") return {"status": "started", "message": f"Scanning episode {request.episode_name}"} except Exception as e: _log("ERROR", f"TV episode scan failed: {e}") raise HTTPException(status_code=500, detail=f"Episode scan failed: {str(e)}") # Debug endpoints @app.get("/debug/movie/{imdb_id}") async def debug_movie(imdb_id: str): """Debug movie processing for specific IMDb ID""" return {"message": f"Debug info for movie {imdb_id} - implementation needed"} @app.get("/debug/movie/{imdb_id}/history") async def debug_movie_history(imdb_id: str): """Debug movie processing history""" return {"message": f"Debug history for movie {imdb_id} - implementation needed"} @app.get("/debug/movie/{imdb_id}/priority") async def debug_movie_priority(imdb_id: str): """Debug movie priority processing""" return {"message": f"Debug priority for movie {imdb_id} - implementation needed"} @app.get("/debug/tmdb/{imdb_id}") async def debug_tmdb(imdb_id: str): """Debug TMDB API calls for movie""" return {"message": f"Debug TMDB for movie {imdb_id} - implementation needed"} # Test endpoints @app.post("/test/bulk-update") async def test_bulk_update(): """Test bulk update functionality""" return {"message": "Test bulk update - implementation needed"} @app.post("/test/movie-scan") async def test_movie_scan(): """Test movie scan functionality""" return {"message": "Test movie scan - implementation needed"} @app.post("/bulk/update") async def bulk_update(): """Bulk update functionality""" return {"message": "Bulk update - implementation needed"}