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