"""Base module containing core VideoArchiver class""" from __future__ import annotations import asyncio import logging from typing import Dict, Any, Optional, TypedDict, ClassVar, List, Set, Union from datetime import datetime from pathlib import Path import discord from redbot.core.bot import Red from redbot.core.commands import GroupCog, Context from .settings import Settings from .lifecycle import LifecycleManager, LifecycleState from .component_manager import ComponentManager, ComponentState from .error_handler import error_manager, handle_command_error from .response_handler import response_manager from .commands.archiver_commands import setup_archiver_commands from .commands.database_commands import setup_database_commands from .commands.settings_commands import setup_settings_commands from .events import setup_events, EventManager from ..processor.core import Processor from ..queue.manager import QueueManager from ..ffmpeg.ffmpeg_manager import FFmpegManager from ..database.video_archive_db import VideoArchiveDB from ..config_manager import ConfigManager from ..utils.exceptions import ( CogError, ErrorContext, ErrorSeverity ) logger = logging.getLogger("VideoArchiver") class CogHealthCheck(TypedDict): """Type definition for health check status""" name: str status: bool last_check: str details: Optional[Dict[str, Any]] class CogStatus(TypedDict): """Type definition for cog status""" uptime: float last_error: Optional[str] error_count: int command_count: int last_command: Optional[str] health_checks: Dict[str, CogHealthCheck] state: str ready: bool class StatusTracker: """Tracks cog status and health""" HEALTH_CHECK_INTERVAL: ClassVar[int] = 30 # Seconds between health checks ERROR_THRESHOLD: ClassVar[int] = 100 # Maximum errors before health warning def __init__(self) -> None: self.start_time = datetime.utcnow() self.last_error: Optional[str] = None self.error_count = 0 self.command_count = 0 self.last_command_time: Optional[datetime] = None self.health_checks: Dict[str, CogHealthCheck] = {} def record_error(self, error: str) -> None: """Record an error occurrence""" self.last_error = error self.error_count += 1 def record_command(self) -> None: """Record a command execution""" self.command_count += 1 self.last_command_time = datetime.utcnow() def update_health_check( self, name: str, status: bool, details: Optional[Dict[str, Any]] = None ) -> None: """Update health check status""" self.health_checks[name] = CogHealthCheck( name=name, status=status, last_check=datetime.utcnow().isoformat(), details=details ) def get_status(self) -> CogStatus: """Get current status""" return CogStatus( uptime=(datetime.utcnow() - self.start_time).total_seconds(), last_error=self.last_error, error_count=self.error_count, command_count=self.command_count, last_command=self.last_command_time.isoformat() if self.last_command_time else None, health_checks=self.health_checks.copy(), state="healthy" if self.is_healthy() else "unhealthy", ready=True ) def is_healthy(self) -> bool: """Check if cog is healthy""" if self.error_count > self.ERROR_THRESHOLD: return False return all(check["status"] for check in self.health_checks.values()) class ComponentAccessor: """Provides safe access to components""" def __init__(self, component_manager: ComponentManager) -> None: self._component_manager = component_manager def get_component(self, name: str) -> Optional[Any]: """ Get a component with state validation. Args: name: Component name Returns: Component instance if ready, None otherwise """ component = self._component_manager.get(name) if component and component.state == ComponentState.READY: return component return None def get_component_status(self, name: str) -> Dict[str, Any]: """ Get component status. Args: name: Component name Returns: Component status dictionary """ return self._component_manager.get_component_status().get(name, {}) class VideoArchiver(GroupCog, Settings): """Archive videos from Discord channels""" def __init__(self, bot: Red) -> None: """Initialize the cog with minimal setup""" super().__init__() self.bot = bot self.ready = asyncio.Event() # Initialize managers self.lifecycle_manager = LifecycleManager(self) self.component_manager = ComponentManager(self) self.component_accessor = ComponentAccessor(self.component_manager) self.status_tracker = StatusTracker() self.event_manager: Optional[EventManager] = None # Initialize task trackers self._init_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None self._queue_task: Optional[asyncio.Task] = None self._health_tasks: Set[asyncio.Task] = set() # Initialize component storage self.components: Dict[int, Dict[str, Any]] = {} self.update_checker = None self._db = None # Set up commands setup_archiver_commands(self) setup_database_commands(self) setup_settings_commands(self) # Set up events self.event_manager = setup_events(self) # Register cleanup handlers self.lifecycle_manager.register_cleanup_handler(self._cleanup_handler) async def cog_load(self) -> None: """ Handle cog loading. Raises: CogError: If loading fails """ try: await self.lifecycle_manager.handle_load() await self._start_health_monitoring() except Exception as e: error = f"Failed to load cog: {str(e)}" self.status_tracker.record_error(error) logger.error(error, exc_info=True) raise CogError( error, context=ErrorContext( "VideoArchiver", "cog_load", None, ErrorSeverity.CRITICAL ) ) async def cog_unload(self) -> None: """ Handle cog unloading. Raises: CogError: If unloading fails """ try: # Cancel health monitoring for task in self._health_tasks: task.cancel() self._health_tasks.clear() await self.lifecycle_manager.handle_unload() except Exception as e: error = f"Failed to unload cog: {str(e)}" self.status_tracker.record_error(error) logger.error(error, exc_info=True) raise CogError( error, context=ErrorContext( "VideoArchiver", "cog_unload", None, ErrorSeverity.CRITICAL ) ) async def cog_command_error( self, ctx: Context, error: Exception ) -> None: """Handle command errors""" self.status_tracker.record_error(str(error)) await handle_command_error(ctx, error) async def cog_before_invoke(self, ctx: Context) -> bool: """Pre-command hook""" self.status_tracker.record_command() return True async def _start_health_monitoring(self) -> None: """Start health monitoring tasks""" self._health_tasks.add( asyncio.create_task(self._monitor_component_health()) ) self._health_tasks.add( asyncio.create_task(self._monitor_system_health()) ) async def _monitor_component_health(self) -> None: """Monitor component health""" while True: try: component_status = self.component_manager.get_component_status() for name, status in component_status.items(): self.status_tracker.update_health_check( f"component_{name}", status["state"] == ComponentState.READY.name, status ) except Exception as e: logger.error(f"Error monitoring component health: {e}", exc_info=True) await asyncio.sleep(self.status_tracker.HEALTH_CHECK_INTERVAL) async def _monitor_system_health(self) -> None: """Monitor system health metrics""" while True: try: # Check queue health if queue_manager := self.queue_manager: queue_status = await queue_manager.get_queue_status() self.status_tracker.update_health_check( "queue_health", queue_status["active"] and not queue_status["stalled"], queue_status ) # Check processor health if processor := self.processor: processor_status = await processor.get_status() self.status_tracker.update_health_check( "processor_health", processor_status["active"], processor_status ) # Check database health if db := self.db: db_status = await db.get_status() self.status_tracker.update_health_check( "database_health", db_status["connected"], db_status ) # Check event system health if self.event_manager: event_stats = self.event_manager.get_stats() self.status_tracker.update_health_check( "event_health", event_stats["health"], event_stats ) except Exception as e: logger.error(f"Error monitoring system health: {e}", exc_info=True) await asyncio.sleep(self.status_tracker.HEALTH_CHECK_INTERVAL) async def _cleanup_handler(self) -> None: """Custom cleanup handler""" try: # Cancel health monitoring tasks for task in self._health_tasks: if not task.done(): task.cancel() try: await task except asyncio.CancelledError: pass self._health_tasks.clear() except Exception as e: logger.error(f"Error in cleanup handler: {e}", exc_info=True) def get_status(self) -> Dict[str, Any]: """ Get comprehensive cog status. Returns: Dictionary containing cog status information """ return { "cog": self.status_tracker.get_status(), "lifecycle": self.lifecycle_manager.get_status(), "components": self.component_manager.get_component_status(), "errors": error_manager.tracker.get_error_stats(), "events": self.event_manager.get_stats() if self.event_manager else None } # Component property accessors @property def processor(self) -> Optional[Processor]: """Get the processor component""" return self.component_accessor.get_component("processor") @property def queue_manager(self) -> Optional[QueueManager]: """Get the queue manager component""" return self.component_accessor.get_component("queue_manager") @property def config_manager(self) -> Optional[ConfigManager]: """Get the config manager component""" return self.component_accessor.get_component("config_manager") @property def ffmpeg_mgr(self) -> Optional[FFmpegManager]: """Get the FFmpeg manager component""" return self.component_accessor.get_component("ffmpeg_mgr") @property def db(self) -> Optional[VideoArchiveDB]: """Get the database component""" return self._db @db.setter def db(self, value: VideoArchiveDB) -> None: """Set the database component""" self._db = value @property def data_path(self) -> Optional[Path]: """Get the data path""" return self.component_accessor.get_component("data_path") @property def download_path(self) -> Optional[Path]: """Get the download path""" return self.component_accessor.get_component("download_path") @property def queue_handler(self): """Get the queue handler from processor""" if processor := self.processor: return processor.queue_handler return None