Files
Pac-cogs/videoarchiver/core/base.py

396 lines
13 KiB
Python

"""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