Files
nfoguard/main.py
T
sbcrumb ab48d485b8
Local Docker Build (Dev) / build-dev (push) Successful in 5s
fix: docker hangs
2025-10-25 12:09:23 -04:00

239 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""
NFOGuard Core - Automated NFO file management and processing engine
Core processing container with webhooks, scanning, and database management
Web interface separated to nfoguard-web container
"""
import os
import sys
import signal
import asyncio
from pathlib import Path
from datetime import datetime, timezone
import uvicorn
from fastapi import FastAPI
# Import configuration first
from config.settings import config
# Authentication removed - handled by separate web container
from utils.logging import _log
# Import core components
from core.database import NFOGuardDatabase
from core.nfo_manager import NFOManager
from core.path_mapper import PathMapper
# Import clients
from clients.external_clients import ExternalClientManager
# Import processors
from processors.tv_processor import TVProcessor
from processors.movie_processor import MovieProcessor
# Import webhook handling
from webhooks.webhook_batcher import WebhookBatcher
# Import API routes
from api.routes import register_routes
# Global shutdown event for graceful shutdown coordination
shutdown_event = asyncio.Event()
def get_version() -> str:
"""Get application version"""
try:
version = (Path(__file__).parent / "VERSION").read_text().strip()
except:
version = "0.1.0"
# Check if running from dev branch (detect at runtime)
try:
# Try to read git branch from .git/HEAD
git_head_path = Path(__file__).parent / ".git" / "HEAD"
if git_head_path.exists():
head_content = git_head_path.read_text().strip()
if "ref: refs/heads/dev" in head_content:
version = f"{version}-dev"
elif head_content.startswith("ref: refs/heads/"):
# Extract branch name for other branches
branch = head_content.split("refs/heads/")[-1]
if branch != "main":
version = f"{version}-{branch}"
except Exception:
# If git detection fails, that's fine - use base version
pass
# Check for build source (only add -gitea for local Gitea builds)
build_source = os.environ.get("BUILD_SOURCE", "")
if build_source == "gitea":
if "gitea" not in version: # Don't double-add gitea suffix
version = f"{version}-gitea"
return version
def create_app() -> FastAPI:
"""Create and configure the FastAPI application"""
version = get_version()
app = FastAPI(
title="NFOGuard",
description="Webhook server for preserving media import dates",
version=version
)
return app
def initialize_components():
"""Initialize all application components"""
start_time = datetime.now(timezone.utc)
# Initialize core components
db = NFOGuardDatabase(config=config)
nfo_manager = NFOManager(config.manager_brand, config.debug)
path_mapper = PathMapper(config)
# Initialize processors
tv_processor = TVProcessor(db, nfo_manager, path_mapper)
movie_processor = MovieProcessor(db, nfo_manager, path_mapper)
# Initialize webhook batcher with nfo_manager for comprehensive IMDb detection
batcher = WebhookBatcher(nfo_manager)
batcher.set_processors(tv_processor, movie_processor)
return {
"db": db,
"nfo_manager": nfo_manager,
"path_mapper": path_mapper,
"tv_processor": tv_processor,
"movie_processor": movie_processor,
"batcher": batcher,
"start_time": start_time,
"config": config,
"version": get_version(),
"shutdown_event": shutdown_event
}
def signal_handler(signum, frame):
"""Handle shutdown signals gracefully"""
_log("INFO", f"Received signal {signum}, shutting down gracefully...")
# Set shutdown event to notify background tasks
shutdown_event.set()
# Get the global dependencies if they exist
if hasattr(signal_handler, 'dependencies') and signal_handler.dependencies:
deps = signal_handler.dependencies
# Shutdown webhook batcher cleanly
if 'batcher' in deps:
try:
_log("INFO", "Shutting down webhook batcher...")
deps['batcher'].shutdown()
except Exception as e:
_log("WARNING", f"Error during batcher shutdown: {e}")
# Close database connection
if 'db' in deps:
try:
_log("INFO", "Closing database connection...")
deps['db'].close()
except Exception as e:
_log("WARNING", f"Error closing database: {e}")
_log("INFO", "Graceful shutdown complete")
# Force exit after 2 seconds if graceful shutdown doesn't work
import threading
def force_exit():
import time
time.sleep(2)
_log("WARNING", "Force exiting after timeout")
os._exit(0)
force_thread = threading.Thread(target=force_exit, daemon=True)
force_thread.start()
sys.exit(0)
def main():
"""Main application entry point"""
# Register signal handlers for graceful shutdown
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
version = get_version()
_log("INFO", "Starting NFOGuard")
_log("INFO", f"Version: {version}")
_log("INFO", f"TV paths: {[str(p) for p in config.tv_paths]}")
_log("INFO", f"Movie paths: {[str(p) for p in config.movie_paths]}")
if config.db_type == "postgresql":
_log("INFO", f"Database: PostgreSQL at {config.db_host}:{config.db_port}/{config.db_name}")
_log("INFO", f"Database user: {config.db_user}")
else:
_log("INFO", f"Database: {config.db_path}")
_log("INFO", f"Config: manage_nfo={config.manage_nfo}, fix_mtimes={config.fix_dir_mtimes}")
_log("INFO", f"Movie priority: {config.movie_priority}")
# Create FastAPI app
app = create_app()
# Initialize components
dependencies = initialize_components()
# Note: Authentication and web interface handled by separate nfoguard-web container
_log("INFO", "Core API: Authentication handled by separate web container")
# Store dependencies globally for signal handler access
signal_handler.dependencies = dependencies
# Register routes
register_routes(app, dependencies)
try:
# Core API configuration (webhooks, processing, database management)
core_host = config.core_api_host if hasattr(config, 'core_api_host') else "0.0.0.0"
core_port = config.core_api_port if hasattr(config, 'core_api_port') else 8080
_log("INFO", f"🚀 Starting NFOGuard Core API on {core_host}:{core_port}")
uvicorn.run(
app,
host=core_host,
port=core_port,
reload=False,
access_log=False, # Reduce logging overhead
server_header=False, # Reduce response overhead
timeout_graceful_shutdown=15 # Give more time for graceful shutdown
)
except KeyboardInterrupt:
_log("INFO", "NFOGuard stopped by user")
except Exception as e:
_log("ERROR", f"NFOGuard crashed: {e}")
sys.exit(1)
finally:
# Ensure cleanup happens even if uvicorn doesn't trigger signal handler
if hasattr(signal_handler, 'dependencies') and signal_handler.dependencies:
deps = signal_handler.dependencies
if 'batcher' in deps:
try:
deps['batcher'].shutdown()
except Exception:
pass
if 'db' in deps:
try:
deps['db'].close()
except Exception:
pass
if __name__ == "__main__":
main()