NFOGuard v2.6.7 - Production ready release

Complete media management solution with PostgreSQL, web interface,
and 88% scan optimization for efficient library management.

Key Features:
- PostgreSQL database with production-grade performance
- Web interface for episode and movie management
- Smart scan optimization reducing scan time by 88%
- Database cleanup tools for orphaned episodes/movies
- Graceful Docker shutdown for container orchestration
- Comprehensive configuration validation
- Real-time health monitoring and metrics
This commit is contained in:
2025-10-20 14:57:36 -04:00
commit 60f7063175
66 changed files with 22575 additions and 0 deletions
View File
+110
View File
@@ -0,0 +1,110 @@
"""
Pydantic models for NFOGuard API
"""
from pydantic import BaseModel
from typing import Optional, Dict, Any, List
class SonarrWebhook(BaseModel):
"""Sonarr webhook payload model"""
eventType: str
series: Optional[Dict[str, Any]] = None
episodes: Optional[list] = []
episodeFile: Optional[Dict[str, Any]] = None
isUpgrade: Optional[bool] = False
class Config:
extra = "allow"
class RadarrWebhook(BaseModel):
"""Radarr webhook payload model"""
eventType: str
movie: Optional[Dict[str, Any]] = None
movieFile: Optional[Dict[str, Any]] = None
isUpgrade: Optional[bool] = False
deletedFiles: Optional[list] = []
remoteMovie: Optional[Dict[str, Any]] = None
renamedMovieFiles: Optional[List[Dict[str, Any]]] = None
class Config:
extra = "allow"
class HealthResponse(BaseModel):
"""Health check response model"""
status: str
version: str
uptime: str
database_status: str
radarr_database: Optional[Dict[str, Any]] = None
class TVSeasonRequest(BaseModel):
"""TV season processing request model"""
series_path: str
season_name: str
class TVEpisodeRequest(BaseModel):
"""TV episode processing request model"""
series_path: str
season: int
episode: int
# Web interface models
class MovieUpdateRequest(BaseModel):
"""Request to update movie dateadded"""
dateadded: Optional[str]
source: str
class EpisodeUpdateRequest(BaseModel):
"""Request to update episode dateadded"""
dateadded: Optional[str]
source: str
class BulkUpdateRequest(BaseModel):
"""Request for bulk source updates"""
media_type: str # "movies" or "episodes"
old_source: str
new_source: str
class MovieResponse(BaseModel):
"""Movie data response"""
imdb_id: str
title: str
path: str
released: Optional[str]
dateadded: Optional[str]
source: Optional[str]
has_video_file: bool
last_updated: str
class SeriesResponse(BaseModel):
"""TV series data response"""
imdb_id: str
title: str
path: str
last_updated: str
total_episodes: int
episodes_with_dates: int
episodes_with_video: int
class EpisodeResponse(BaseModel):
"""TV episode data response"""
season: int
episode: int
aired: Optional[str]
dateadded: Optional[str]
source: Optional[str]
has_video_file: bool
last_updated: str
series_path: str
season_name: str
episode_name: str
+351
View File
@@ -0,0 +1,351 @@
"""
Monitoring API Routes for NFOGuard
Provides health checks, metrics, and system status endpoints
"""
from fastapi import APIRouter, Response, HTTPException
from typing import Dict, Any, Optional
import time
from monitoring.health import health_checker, HealthStatus
from monitoring.metrics import metrics
try:
from config.validator import get_configuration_summary
except ImportError:
def get_configuration_summary():
return {"status": "Configuration validator not available"}
router = APIRouter(prefix="/api/v1", tags=["monitoring"])
@router.get("/health")
async def get_health_status():
"""
Get comprehensive health status
Returns detailed health information including:
- Overall system health
- Individual component health checks
- Performance metrics
- Error status
"""
try:
health_status = await health_checker.get_full_health_status()
# Set appropriate HTTP status code
if health_status.status == HealthStatus.HEALTHY:
status_code = 200
elif health_status.status == HealthStatus.DEGRADED:
status_code = 200 # Still operational
else: # UNHEALTHY
status_code = 503 # Service unavailable
return Response(
content=health_status.to_dict(),
status_code=status_code,
media_type="application/json"
)
except Exception as e:
# Return unhealthy status if health check itself fails
return Response(
content={
"status": "unhealthy",
"message": f"Health check failed: {e}",
"timestamp": time.time()
},
status_code=500,
media_type="application/json"
)
@router.get("/health/ready")
async def get_readiness_status():
"""
Kubernetes readiness probe endpoint
Returns 200 if service is ready to accept traffic
Returns 503 if service is not ready
"""
try:
readiness = await health_checker.get_readiness_status()
status_code = 200 if readiness["ready"] else 503
return Response(
content=readiness,
status_code=status_code,
media_type="application/json"
)
except Exception as e:
return Response(
content={
"ready": False,
"message": f"Readiness check failed: {e}",
"timestamp": time.time()
},
status_code=503,
media_type="application/json"
)
@router.get("/health/live")
async def get_liveness_status():
"""
Kubernetes liveness probe endpoint
Returns 200 if service is alive and responsive
Returns 500 if service should be restarted
"""
try:
liveness = await health_checker.get_liveness_status()
status_code = 200 if liveness["alive"] else 500
return Response(
content=liveness,
status_code=status_code,
media_type="application/json"
)
except Exception as e:
return Response(
content={
"alive": False,
"message": f"Liveness check failed: {e}",
"timestamp": time.time()
},
status_code=500,
media_type="application/json"
)
@router.get("/metrics")
async def get_prometheus_metrics():
"""
Prometheus-compatible metrics endpoint
Returns metrics in Prometheus text format for scraping
"""
try:
prometheus_metrics = metrics.get_prometheus_metrics()
return Response(
content=prometheus_metrics,
media_type="text/plain; charset=utf-8"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to generate metrics: {e}"
)
@router.get("/metrics/json")
async def get_metrics_json():
"""
Get all metrics in JSON format
Returns structured metrics data including:
- System metrics (CPU, memory, disk)
- Processing metrics (rates, durations)
- Error metrics (counts, recent errors)
"""
try:
all_metrics = metrics.get_all_metrics()
return all_metrics
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get metrics: {e}"
)
@router.get("/status")
async def get_system_status():
"""
Get comprehensive system status
Returns detailed system information including:
- Health status
- Configuration summary
- Performance metrics
- Recent activity
"""
try:
# Get health status
health_status = await health_checker.get_full_health_status()
# Get metrics
all_metrics = metrics.get_all_metrics()
# Get configuration summary
config_summary = get_configuration_summary()
# Combine into comprehensive status
status_response = {
"overall_status": health_status.status.value,
"timestamp": time.time(),
"uptime_seconds": health_status.uptime_seconds,
"version": health_status.version,
"health": health_status.to_dict(),
"metrics": all_metrics,
"configuration": config_summary,
"summary": {
"service_healthy": health_status.status in [HealthStatus.HEALTHY, HealthStatus.DEGRADED],
"total_webhooks_processed": all_metrics["processing"]["total_webhooks"],
"total_nfo_files_created": all_metrics["processing"]["total_nfo_created"],
"total_errors": all_metrics["processing"]["total_errors"],
"current_processing_rate": all_metrics["processing"]["webhooks_received_per_minute"],
"average_processing_time": all_metrics["processing"]["average_processing_time_seconds"]
}
}
return status_response
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get system status: {e}"
)
@router.get("/status/brief")
async def get_brief_status():
"""
Get brief system status for quick monitoring
Returns essential status information without detailed metrics
"""
try:
# Get basic health
liveness = await health_checker.get_liveness_status()
readiness = await health_checker.get_readiness_status()
# Get basic metrics
processing_metrics = metrics.get_processing_metrics()
system_metrics = metrics.get_system_metrics()
return {
"status": "healthy" if liveness["alive"] and readiness["ready"] else "unhealthy",
"alive": liveness["alive"],
"ready": readiness["ready"],
"uptime_seconds": liveness["uptime_seconds"],
"webhooks_per_minute": processing_metrics["webhooks_received_per_minute"],
"active_operations": processing_metrics["active_operations"],
"total_errors": processing_metrics["total_errors"],
"cpu_percent": system_metrics.get("cpu_percent", 0),
"memory_percent": system_metrics.get("memory_percent", 0),
"timestamp": time.time()
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get brief status: {e}"
)
@router.get("/metrics/processing")
async def get_processing_metrics():
"""
Get processing-specific metrics
Returns metrics focused on NFO processing performance
"""
try:
processing_metrics = metrics.get_processing_metrics()
return processing_metrics
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get processing metrics: {e}"
)
@router.get("/metrics/errors")
async def get_error_metrics():
"""
Get error-specific metrics
Returns error counts, types, and recent error information
"""
try:
error_metrics = metrics.get_error_metrics()
return error_metrics
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get error metrics: {e}"
)
@router.get("/metrics/system")
async def get_system_metrics():
"""
Get system resource metrics
Returns CPU, memory, disk, and process information
"""
try:
system_metrics = metrics.get_system_metrics()
return system_metrics
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get system metrics: {e}"
)
@router.post("/metrics/reset")
async def reset_metrics(metric_types: Optional[str] = None):
"""
Reset specific metric types
Parameters:
- metric_types: Comma-separated list of metric types to reset
(counters, histograms, errors, timeseries)
If not specified, resets all metrics
"""
try:
reset_types = None
if metric_types:
reset_types = [t.strip() for t in metric_types.split(",")]
metrics.reset_metrics(reset_types)
return {
"message": "Metrics reset successfully",
"reset_types": reset_types or "all",
"timestamp": time.time()
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to reset metrics: {e}"
)
# Legacy endpoints for backwards compatibility
@router.get("/health-check")
async def legacy_health_check():
"""Legacy health check endpoint (redirects to /health)"""
return await get_brief_status()
@router.get("/ping")
async def ping():
"""Simple ping endpoint for basic connectivity testing"""
return {
"message": "pong",
"timestamp": time.time(),
"service": "nfoguard",
"version": "2.0.0"
}
+1442
View File
File diff suppressed because it is too large Load Diff
+1216
View File
File diff suppressed because it is too large Load Diff