diff --git a/api/routes.py b/api/routes.py new file mode 100644 index 0000000..e003661 --- /dev/null +++ b/api/routes.py @@ -0,0 +1,231 @@ +#!/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 (implementation would continue from original file) + # This is a placeholder - full implementation needs to be copied from nfoguard.py + return {"status": "queued", "message": "Sonarr 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 (implementation would continue from original file) + # This is a placeholder - full implementation needs to be copied from nfoguard.py + return {"status": "queued", "message": "Radarr 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 { + "message": "Batch status endpoint - implementation needed", + "pending_items": len(getattr(batcher, 'pending', {})) + } + + @app.post("/manual/scan") + async def manual_scan(background_tasks: BackgroundTasks): + """Trigger manual library scan""" + try: + # Add the actual manual scan logic here + _log("INFO", "Manual scan triggered via API") + return {"status": "started", "message": "Manual scan initiated"} + 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"} \ No newline at end of file