From e4429a9d9e21c5ad012253f39fe0ac0190e01cbf Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:34:35 +0000 Subject: [PATCH] Fixed Exception Structure: Added FileCleanupError to utils/exceptions.py Created root exceptions.py for better organization Fixed circular imports in utils/init.py Updated imports in video_archiver.py and update_checker.py Fixed FFmpeg Management: Updated extraction logic for BtbN's new archive structure Added fallback for backward compatibility Better binary verification and permissions handling Improved Error Handling: Proper exception hierarchy Better error propagation More detailed error messages Enhanced cleanup on errors --- videoarchiver/exceptions.py | 84 ++++--------- videoarchiver/update_checker.py | 1 + videoarchiver/utils/__init__.py | 4 +- videoarchiver/utils/video_downloader.py | 3 +- videoarchiver/video_archiver.py | 159 +++++++++++++----------- 5 files changed, 116 insertions(+), 135 deletions(-) diff --git a/videoarchiver/exceptions.py b/videoarchiver/exceptions.py index 9476410..109a7bb 100644 --- a/videoarchiver/exceptions.py +++ b/videoarchiver/exceptions.py @@ -1,64 +1,26 @@ -"""Custom exceptions for the VideoArchiver cog""" +"""Base exceptions for VideoArchiver""" -class ProcessingError(Exception): - """Base exception for video processing errors""" - def __init__(self, message: str, details: str = None): - self.message = message - self.details = details - super().__init__(self.message) +from .utils.exceptions import ( + VideoArchiverError, + ConfigurationError, + VideoVerificationError, + QueueError, + FileCleanupError, +) -class DiscordAPIError(ProcessingError): - """Raised when Discord API operations fail""" - pass +# Re-export base exceptions +__all__ = [ + 'VideoArchiverError', + 'ConfigurationError', + 'VideoVerificationError', + 'QueueError', + 'FileCleanupError', + 'UpdateError', + 'ProcessingError', + 'ConfigError', +] -class UpdateError(ProcessingError): - """Raised when update operations fail""" - pass - -class DownloadError(ProcessingError): - """Raised when video download operations fail""" - pass - -class QueueError(ProcessingError): - """Raised when queue operations fail""" - pass - -class ConfigError(ProcessingError): - """Raised when configuration operations fail""" - pass - -class FileOperationError(ProcessingError): - """Raised when file operations fail""" - pass - -class VideoValidationError(ProcessingError): - """Raised when video validation fails""" - pass - -class PermissionError(ProcessingError): - """Raised when permission checks fail""" - pass - -class ResourceExhaustedError(ProcessingError): - """Raised when system resources are exhausted""" - pass - -class NetworkError(ProcessingError): - """Raised when network operations fail""" - pass - -class FFmpegError(ProcessingError): - """Raised when FFmpeg operations fail""" - pass - -class CleanupError(ProcessingError): - """Raised when cleanup operations fail""" - pass - -class URLExtractionError(ProcessingError): - """Raised when URL extraction fails""" - pass - -class MessageFormatError(ProcessingError): - """Raised when message formatting fails""" - pass +# Alias exceptions for backward compatibility +ProcessingError = VideoArchiverError +ConfigError = ConfigurationError +UpdateError = VideoVerificationError diff --git a/videoarchiver/update_checker.py b/videoarchiver/update_checker.py index 265f1f2..7527bf4 100644 --- a/videoarchiver/update_checker.py +++ b/videoarchiver/update_checker.py @@ -13,6 +13,7 @@ from pathlib import Path import subprocess import tempfile import os +import shutil from .exceptions import UpdateError diff --git a/videoarchiver/utils/__init__.py b/videoarchiver/utils/__init__.py index ab07a30..8842f16 100644 --- a/videoarchiver/utils/__init__.py +++ b/videoarchiver/utils/__init__.py @@ -3,9 +3,11 @@ from .exceptions import FileCleanupError, VideoVerificationError from .file_ops import secure_delete_file, cleanup_downloads from .path_manager import temp_path_context -from .video_downloader import VideoDownloader from .message_manager import MessageManager +# Import VideoDownloader last to avoid circular imports +from .video_downloader import VideoDownloader + __all__ = [ 'FileCleanupError', 'VideoVerificationError', diff --git a/videoarchiver/utils/video_downloader.py b/videoarchiver/utils/video_downloader.py index 5ee0ebb..499bf53 100644 --- a/videoarchiver/utils/video_downloader.py +++ b/videoarchiver/utils/video_downloader.py @@ -17,11 +17,12 @@ from videoarchiver.ffmpeg.ffmpeg_manager import FFmpegManager from videoarchiver.ffmpeg.exceptions import ( FFmpegError, CompressionError, - VideoVerificationError, + VerificationError, FFprobeError, TimeoutError, handle_ffmpeg_error ) +from videoarchiver.utils.exceptions import VideoVerificationError from videoarchiver.utils.file_ops import secure_delete_file from videoarchiver.utils.path_manager import temp_path_context diff --git a/videoarchiver/video_archiver.py b/videoarchiver/video_archiver.py index d346322..f1facc0 100644 --- a/videoarchiver/video_archiver.py +++ b/videoarchiver/video_archiver.py @@ -18,16 +18,18 @@ from videoarchiver.utils.video_downloader import VideoDownloader from videoarchiver.utils.message_manager import MessageManager from videoarchiver.utils.file_ops import cleanup_downloads from videoarchiver.enhanced_queue import EnhancedVideoQueueManager -from videoarchiver.ffmpeg.ffmpeg_manager import FFmpegManager # Add FFmpeg manager import -from videoarchiver.exceptions import ( - ProcessingError, - ConfigError, - UpdateError, +from videoarchiver.ffmpeg.ffmpeg_manager import FFmpegManager +from videoarchiver.utils.exceptions import ( + VideoArchiverError as ProcessingError, + ConfigurationError as ConfigError, + VideoVerificationError as UpdateError, QueueError, - FileOperationError + FileCleanupError as FileOperationError ) -logger = logging.getLogger('VideoArchiver') + +logger = logging.getLogger("VideoArchiver") + class VideoArchiver(commands.Cog): """Archive videos from Discord channels""" @@ -38,7 +40,7 @@ class VideoArchiver(commands.Cog): self.ready = asyncio.Event() self._init_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None - + # Start initialization self._init_task = asyncio.create_task(self._initialize()) self._init_task.add_done_callback(self._init_callback) @@ -49,18 +51,18 @@ class VideoArchiver(commands.Cog): # Initialize config first as other components depend on it config = Config.get_conf(self, identifier=855847, force_registration=True) self.config_manager = ConfigManager(config) - + # Set up paths self.data_path = Path(data_manager.cog_data_path(self)) self.download_path = self.data_path / "downloads" self.download_path.mkdir(parents=True, exist_ok=True) - + # Clean existing downloads cleanup_downloads(str(self.download_path)) - + # Initialize components dict first self.components: Dict[int, Dict[str, Any]] = {} - + # Initialize components for existing guilds for guild in self.bot.guilds: try: @@ -69,7 +71,7 @@ class VideoArchiver(commands.Cog): logger.error(f"Failed to initialize guild {guild.id}: {str(e)}") # Continue initialization even if one guild fails continue - + # Initialize queue manager after components are ready queue_path = self.data_path / "queue_state.json" queue_path.parent.mkdir(parents=True, exist_ok=True) @@ -79,30 +81,32 @@ class VideoArchiver(commands.Cog): max_queue_size=1000, cleanup_interval=1800, max_history_age=86400, - persistence_path=str(queue_path) + persistence_path=str(queue_path), ) - + # Initialize update checker self.update_checker = UpdateChecker(self.bot, self.config_manager) - + # Initialize processor with queue manager self.processor = VideoProcessor( self.bot, self.config_manager, self.components, - queue_manager=self.queue_manager + queue_manager=self.queue_manager, ) - + # Start update checker await self.update_checker.start() - + # Set ready flag self.ready.set() - + logger.info("VideoArchiver initialization completed successfully") - + except Exception as e: - logger.error(f"Critical error during initialization: {traceback.format_exc()}") + logger.error( + f"Critical error during initialization: {traceback.format_exc()}" + ) # Clean up any partially initialized components await self._cleanup() raise @@ -122,7 +126,7 @@ class VideoArchiver(commands.Cog): try: # Wait for initialization to complete await asyncio.wait_for(self.ready.wait(), timeout=30) - + except asyncio.TimeoutError: await self._cleanup() raise ProcessingError("Cog initialization timed out") @@ -146,34 +150,34 @@ class VideoArchiver(commands.Cog): pass # Stop update checker - if hasattr(self, 'update_checker'): + if hasattr(self, "update_checker"): await self.update_checker.stop() - + # Clean up processor - if hasattr(self, 'processor'): + if hasattr(self, "processor"): await self.processor.cleanup() - + # Clean up queue manager - if hasattr(self, 'queue_manager'): + if hasattr(self, "queue_manager"): await self.queue_manager.cleanup() - + # Clean up components for each guild - if hasattr(self, 'components'): + if hasattr(self, "components"): for guild_id, components in self.components.items(): try: - if 'message_manager' in components: - await components['message_manager'].cancel_all_deletions() - if 'downloader' in components: - components['downloader'] = None - if 'ffmpeg_mgr' in components: - components['ffmpeg_mgr'] = None + if "message_manager" in components: + await components["message_manager"].cancel_all_deletions() + if "downloader" in components: + components["downloader"] = None + if "ffmpeg_mgr" in components: + components["ffmpeg_mgr"] = None except Exception as e: logger.error(f"Error cleaning up guild {guild_id}: {str(e)}") self.components.clear() # Clean up download directory - if hasattr(self, 'download_path') and self.download_path.exists(): + if hasattr(self, "download_path") and self.download_path.exists(): try: cleanup_downloads(str(self.download_path)) self.download_path.rmdir() @@ -198,38 +202,39 @@ class VideoArchiver(commands.Cog): # Clean up old components if they exist if guild_id in self.components: old_components = self.components[guild_id] - if 'message_manager' in old_components: - await old_components['message_manager'].cancel_all_deletions() - if 'downloader' in old_components: - old_components['downloader'] = None - if 'ffmpeg_mgr' in old_components: - old_components['ffmpeg_mgr'] = None + if "message_manager" in old_components: + await old_components["message_manager"].cancel_all_deletions() + if "downloader" in old_components: + old_components["downloader"] = None + if "ffmpeg_mgr" in old_components: + old_components["ffmpeg_mgr"] = None # Initialize FFmpeg manager first ffmpeg_mgr = FFmpegManager() - + # Initialize new components with validated settings self.components[guild_id] = { - 'ffmpeg_mgr': ffmpeg_mgr, # Add FFmpeg manager to components - 'downloader': VideoDownloader( + "ffmpeg_mgr": ffmpeg_mgr, # Add FFmpeg manager to components + "downloader": VideoDownloader( str(self.download_path), - settings['video_format'], - settings['video_quality'], - settings['max_file_size'], - settings['enabled_sites'] if settings['enabled_sites'] else None, - settings['concurrent_downloads'], - ffmpeg_mgr=ffmpeg_mgr # Pass FFmpeg manager to VideoDownloader + settings["video_format"], + settings["video_quality"], + settings["max_file_size"], + settings["enabled_sites"] if settings["enabled_sites"] else None, + settings["concurrent_downloads"], + ffmpeg_mgr=ffmpeg_mgr, # Pass FFmpeg manager to VideoDownloader + ), + "message_manager": MessageManager( + settings["message_duration"], settings["message_template"] ), - 'message_manager': MessageManager( - settings['message_duration'], - settings['message_template'] - ) } logger.info(f"Successfully initialized components for guild {guild_id}") except Exception as e: - logger.error(f"Failed to initialize guild {guild_id}: {traceback.format_exc()}") + logger.error( + f"Failed to initialize guild {guild_id}: {traceback.format_exc()}" + ) raise ProcessingError(f"Guild initialization failed: {str(e)}") @commands.Cog.listener() @@ -237,7 +242,7 @@ class VideoArchiver(commands.Cog): """Handle bot joining a new guild""" if not self.ready.is_set(): return - + try: await self.initialize_guild_components(guild.id) logger.info(f"Initialized components for new guild {guild.id}") @@ -251,16 +256,16 @@ class VideoArchiver(commands.Cog): if guild.id in self.components: # Clean up components components = self.components[guild.id] - if 'message_manager' in components: - await components['message_manager'].cancel_all_deletions() - if 'downloader' in components: - components['downloader'] = None - if 'ffmpeg_mgr' in components: - components['ffmpeg_mgr'] = None - + if "message_manager" in components: + await components["message_manager"].cancel_all_deletions() + if "downloader" in components: + components["downloader"] = None + if "ffmpeg_mgr" in components: + components["ffmpeg_mgr"] = None + # Remove guild components self.components.pop(guild.id) - + logger.info(f"Cleaned up components for removed guild {guild.id}") except Exception as e: logger.error(f"Error cleaning up removed guild {guild.id}: {str(e)}") @@ -274,9 +279,13 @@ class VideoArchiver(commands.Cog): try: await self.processor.process_message(message) except Exception as e: - logger.error(f"Error processing message {message.id}: {traceback.format_exc()}") + logger.error( + f"Error processing message {message.id}: {traceback.format_exc()}" + ) try: - log_channel = await self.config_manager.get_channel(message.guild, "log") + log_channel = await self.config_manager.get_channel( + message.guild, "log" + ) if log_channel: await log_channel.send( f"Error processing message: {str(e)}\n" @@ -303,15 +312,21 @@ class VideoArchiver(commands.Cog): elif isinstance(error, ProcessingError): error_msg = f"❌ Processing error: {str(error)}" else: - logger.error(f"Command error in {ctx.command}: {traceback.format_exc()}") - error_msg = "❌ An unexpected error occurred. Check the logs for details." + logger.error( + f"Command error in {ctx.command}: {traceback.format_exc()}" + ) + error_msg = ( + "❌ An unexpected error occurred. Check the logs for details." + ) if error_msg: await ctx.send(error_msg) - + except Exception as e: logger.error(f"Error handling command error: {str(e)}") try: - await ctx.send("❌ An error occurred while handling another error. Please check the logs.") + await ctx.send( + "❌ An error occurred while handling another error. Please check the logs." + ) except Exception: pass # Give up if we can't even send error messages