Files
nfoguard/api/routes.py
T
sbcrumb 70302837a7
Local Docker Build (Dev) / build-dev (push) Successful in 28s
huge: update
2025-09-25 18:41:37 -04:00

263 lines
11 KiB
Python

#!/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"}
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"}