Core Functionality:

base.py: Streamlined to core cog functionality and lifecycle management
initialization.py: Centralized initialization logic
error_handler.py: Unified error handling
response_handler.py: Consistent response handling
Command Organization:

commands/archiver_commands.py: Core archiver functionality
commands/database_commands.py: Database operations
commands/settings_commands.py: Settings management
All commands properly integrated with the cog using setup functions
Improved Architecture:

Consistent command registration pattern
Better separation of concerns
Maintained all original functionality
Enhanced error handling and response management
Proper command integration with the cog structure
This commit is contained in:
pacnpal
2024-11-16 03:30:11 +00:00
parent 6aeb87fb40
commit 537a325807
8 changed files with 824 additions and 623 deletions

View File

@@ -2,61 +2,29 @@
from __future__ import annotations from __future__ import annotations
import discord
import traceback
from redbot.core import Config, data_manager
from redbot.core.bot import Red
from redbot.core.commands import (
GroupCog,
Context,
hybrid_command,
hybrid_group,
guild_only,
commands,
MissingPermissions,
BotMissingPermissions,
MissingRequiredArgument,
BadArgument,
)
from redbot.core import checks
from discord import app_commands
import logging
import asyncio import asyncio
import logging
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional from redbot.core.bot import Red
from redbot.core.commands import GroupCog
from ..config_manager import ConfigManager from .initialization import initialize_cog, init_callback
from ..update_checker import UpdateChecker from .error_handler import handle_command_error
from ..processor import VideoProcessor
from ..utils.video_downloader import VideoDownloader
from ..utils.message_manager import MessageManager
from ..utils.file_ops import cleanup_downloads
from ..queue import EnhancedVideoQueueManager
from ..ffmpeg.ffmpeg_manager import FFmpegManager
from ..database.video_archive_db import VideoArchiveDB
from ..utils.exceptions import (
VideoArchiverError as ProcessingError,
ConfigurationError as ConfigError,
)
from .guild import initialize_guild_components
from .cleanup import cleanup_resources, force_cleanup_resources from .cleanup import cleanup_resources, force_cleanup_resources
from .events import setup_events from .commands import setup_archiver_commands, setup_database_commands, setup_settings_commands
from ..utils.exceptions import VideoArchiverError as ProcessingError
logger = logging.getLogger("VideoArchiver") logger = logging.getLogger("VideoArchiver")
# Constants for timeouts - more reasonable timeouts # Constants for timeouts
UNLOAD_TIMEOUT = 30 # seconds UNLOAD_TIMEOUT = 30 # seconds
CLEANUP_TIMEOUT = 15 # seconds CLEANUP_TIMEOUT = 15 # seconds
INIT_TIMEOUT = 60 # seconds
COMPONENT_INIT_TIMEOUT = 30 # seconds
class VideoArchiver(GroupCog): class VideoArchiver(GroupCog):
"""Archive videos from Discord channels""" """Archive videos from Discord channels"""
default_guild_settings = { default_guild_settings = {
"enabled": False, # Changed to match config_manager.py "enabled": False,
"archive_channel": None, "archive_channel": None,
"log_channel": None, "log_channel": None,
"enabled_channels": [], # Empty list means all channels "enabled_channels": [], # Empty list means all channels
@@ -90,581 +58,21 @@ class VideoArchiver(GroupCog):
self.data_path = None self.data_path = None
self.download_path = None self.download_path = None
# Set up commands
setup_archiver_commands(self)
setup_database_commands(self)
setup_settings_commands(self)
# Set up events - non-blocking # Set up events - non-blocking
from .events import setup_events
setup_events(self) setup_events(self)
async def _handle_response(self, ctx: Context, content: str = None, embed: discord.Embed = None) -> None:
"""Helper method to handle responses for both regular commands and interactions"""
try:
if hasattr(ctx, 'interaction') and ctx.interaction:
try:
# Check if this is a deferred interaction
if ctx.interaction.response.is_done():
# Use followup for deferred responses
if embed:
await ctx.followup.send(content=content, embed=embed)
else:
await ctx.followup.send(content=content)
else:
# Use regular response for non-deferred interactions
if embed:
await ctx.interaction.response.send_message(content=content, embed=embed)
else:
await ctx.interaction.response.send_message(content=content)
except discord.errors.InteractionResponded:
# If we get here, the interaction was already responded to
# Use followup as a fallback
if embed:
await ctx.followup.send(content=content, embed=embed)
else:
await ctx.followup.send(content=content)
else:
# Regular command response
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
except Exception as e:
logger.error(f"Error sending response: {e}")
# If all else fails, try to send a regular message
try:
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
except Exception as e2:
logger.error(f"Failed to send fallback message: {e2}")
@hybrid_group(name="archivedb", fallback="help")
@guild_only()
async def archivedb(self, ctx: Context):
"""Manage the video archive database."""
if ctx.invoked_subcommand is None:
await self._handle_response(ctx, "Use `/help archivedb` for a list of commands.")
@archivedb.command(name="enable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def enable_database(self, ctx: Context):
"""Enable the video archive database."""
try:
# Check if database is already enabled
current_setting = await self.config_manager.get_setting(
ctx.guild.id, "use_database"
)
if current_setting:
await self._handle_response(ctx, "The video archive database is already enabled.")
return
# Initialize database
self.db = VideoArchiveDB(self.data_path)
# Update processor with database
if self.processor:
self.processor.db = self.db
if self.processor.queue_handler:
self.processor.queue_handler.db = self.db
# Update setting
await self.config_manager.update_setting(ctx.guild.id, "use_database", True)
# Send success message
await self._handle_response(ctx, "Video archive database has been enabled.")
except Exception as e:
logger.error(f"Error enabling database: {e}")
await self._handle_response(ctx, "An error occurred while enabling the database. Please check the logs for details.")
@archivedb.command(name="disable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def disable_database(self, ctx: Context):
"""Disable the video archive database."""
try:
current_setting = await self.config_manager.get_setting(
ctx.guild.id, "use_database"
)
if not current_setting:
await self._handle_response(ctx, "The video archive database is already disabled.")
return
# Remove database references
self.db = None
self.processor.db = None
self.processor.queue_handler.db = None
await self.config_manager.update_setting(
ctx.guild.id, "use_database", False
)
await self._handle_response(ctx, "Video archive database has been disabled.")
except Exception as e:
logger.error(f"Error disabling database: {e}")
await self._handle_response(ctx, "An error occurred while disabling the database.")
@hybrid_command()
@guild_only()
@app_commands.describe(url="The URL of the video to check")
async def checkarchived(self, ctx: Context, url: str):
"""Check if a video URL has been archived and get its Discord link if it exists."""
try:
if not self.db:
await self._handle_response(
ctx,
"The archive database is not enabled. Ask an admin to enable it with `/archivedb enable`"
)
return
result = self.db.get_archived_video(url)
if result:
discord_url, message_id, channel_id, guild_id = result
embed = discord.Embed(
title="Video Found in Archive",
description=f"This video has been archived!\n\nOriginal URL: {url}",
color=discord.Color.green(),
)
embed.add_field(name="Archived Link", value=discord_url)
await self._handle_response(ctx, embed=embed)
else:
embed = discord.Embed(
title="Video Not Found",
description="This video has not been archived yet.",
color=discord.Color.red(),
)
await self._handle_response(ctx, embed=embed)
except Exception as e:
logger.error(f"Error checking archived video: {e}")
await self._handle_response(ctx, "An error occurred while checking the archive.")
@hybrid_group(name="archiver", fallback="help")
@guild_only()
async def archiver(self, ctx: Context):
"""Manage video archiver settings."""
if ctx.invoked_subcommand is None:
await self._handle_response(ctx, "Use `/help archiver` for a list of commands.")
@archiver.command(name="enable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def enable_archiver(self, ctx: Context):
"""Enable video archiving in this server."""
try:
current_setting = await self.config_manager.get_setting(
ctx.guild.id, "enabled"
)
if current_setting:
await self._handle_response(ctx, "Video archiving is already enabled.")
return
await self.config_manager.update_setting(ctx.guild.id, "enabled", True)
await self._handle_response(ctx, "Video archiving has been enabled.")
except Exception as e:
logger.error(f"Error enabling archiver: {e}")
await self._handle_response(ctx, "An error occurred while enabling video archiving.")
@archiver.command(name="disable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def disable_archiver(self, ctx: Context):
"""Disable video archiving in this server."""
try:
current_setting = await self.config_manager.get_setting(
ctx.guild.id, "enabled"
)
if not current_setting:
await self._handle_response(ctx, "Video archiving is already disabled.")
return
await self.config_manager.update_setting(ctx.guild.id, "enabled", False)
await self._handle_response(ctx, "Video archiving has been disabled.")
except Exception as e:
logger.error(f"Error disabling archiver: {e}")
await self._handle_response(ctx, "An error occurred while disabling video archiving.")
@archiver.command(name="setchannel")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel where archived videos will be stored")
async def set_archive_channel(self, ctx: Context, channel: discord.TextChannel):
"""Set the channel where archived videos will be stored."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
await self.config_manager.update_setting(
ctx.guild.id, "archive_channel", channel.id
)
await self._handle_response(ctx, f"Archive channel has been set to {channel.mention}.")
except Exception as e:
logger.error(f"Error setting archive channel: {e}")
await self._handle_response(ctx, "An error occurred while setting the archive channel.")
@archiver.command(name="setlog")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel where log messages will be sent")
async def set_log_channel(self, ctx: Context, channel: discord.TextChannel):
"""Set the channel where log messages will be sent."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
await self.config_manager.update_setting(
ctx.guild.id, "log_channel", channel.id
)
await self._handle_response(ctx, f"Log channel has been set to {channel.mention}.")
except Exception as e:
logger.error(f"Error setting log channel: {e}")
await self._handle_response(ctx, "An error occurred while setting the log channel.")
@archiver.command(name="addchannel")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel to monitor for videos")
async def add_enabled_channel(self, ctx: Context, channel: discord.TextChannel):
"""Add a channel to monitor for videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
enabled_channels = await self.config_manager.get_setting(
ctx.guild.id, "enabled_channels"
)
if channel.id in enabled_channels:
await self._handle_response(ctx, f"{channel.mention} is already being monitored.")
return
enabled_channels.append(channel.id)
await self.config_manager.update_setting(
ctx.guild.id, "enabled_channels", enabled_channels
)
await self._handle_response(ctx, f"Now monitoring {channel.mention} for videos.")
except Exception as e:
logger.error(f"Error adding enabled channel: {e}")
await self._handle_response(ctx, "An error occurred while adding the channel.")
@archiver.command(name="removechannel")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel to stop monitoring")
async def remove_enabled_channel(self, ctx: Context, channel: discord.TextChannel):
"""Remove a channel from video monitoring."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
enabled_channels = await self.config_manager.get_setting(
ctx.guild.id, "enabled_channels"
)
if channel.id not in enabled_channels:
await self._handle_response(ctx, f"{channel.mention} is not being monitored.")
return
enabled_channels.remove(channel.id)
await self.config_manager.update_setting(
ctx.guild.id, "enabled_channels", enabled_channels
)
await self._handle_response(ctx, f"Stopped monitoring {channel.mention} for videos.")
except Exception as e:
logger.error(f"Error removing enabled channel: {e}")
await self._handle_response(ctx, "An error occurred while removing the channel.")
@archiver.command(name="setformat")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(format="The video format to use (mp4, webm, or mkv)")
async def set_video_format(self, ctx: Context, format: str):
"""Set the video format for archived videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
format = format.lower()
if format not in ["mp4", "webm", "mkv"]:
await self._handle_response(ctx, "Invalid format. Please use mp4, webm, or mkv.")
return
await self.config_manager.update_setting(ctx.guild.id, "video_format", format)
await self._handle_response(ctx, f"Video format has been set to {format}.")
except Exception as e:
logger.error(f"Error setting video format: {e}")
await self._handle_response(ctx, "An error occurred while setting the video format.")
@archiver.command(name="setquality")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(quality="The video quality (144-4320)")
async def set_video_quality(self, ctx: Context, quality: int):
"""Set the video quality for archived videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
if not 144 <= quality <= 4320:
await self._handle_response(ctx, "Quality must be between 144 and 4320.")
return
await self.config_manager.update_setting(ctx.guild.id, "video_quality", quality)
await self._handle_response(ctx, f"Video quality has been set to {quality}p.")
except Exception as e:
logger.error(f"Error setting video quality: {e}")
await self._handle_response(ctx, "An error occurred while setting the video quality.")
@archiver.command(name="setmaxsize")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(size="The maximum file size in MB (1-100)")
async def set_max_file_size(self, ctx: Context, size: int):
"""Set the maximum file size for archived videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
if not 1 <= size <= 100:
await self._handle_response(ctx, "Size must be between 1 and 100 MB.")
return
await self.config_manager.update_setting(ctx.guild.id, "max_file_size", size)
await self._handle_response(ctx, f"Maximum file size has been set to {size}MB.")
except Exception as e:
logger.error(f"Error setting max file size: {e}")
await self._handle_response(ctx, "An error occurred while setting the maximum file size.")
@archiver.command(name="setmessageduration")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(hours="How long to keep messages in hours (0-168)")
async def set_message_duration(self, ctx: Context, hours: int):
"""Set how long to keep archived messages."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
if not 0 <= hours <= 168:
await self._handle_response(ctx, "Duration must be between 0 and 168 hours (1 week).")
return
await self.config_manager.update_setting(ctx.guild.id, "message_duration", hours)
await self._handle_response(ctx, f"Message duration has been set to {hours} hours.")
except Exception as e:
logger.error(f"Error setting message duration: {e}")
await self._handle_response(ctx, "An error occurred while setting the message duration.")
@archiver.command(name="settemplate")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(template="The message template to use. Available placeholders: {author}, {channel}, {original_message}")
async def set_message_template(self, ctx: Context, template: str):
"""Set the template for archived messages."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
if not any(
ph in template for ph in ["{author}", "{channel}", "{original_message}"]
):
await self._handle_response(
ctx,
"Template must include at least one placeholder: {author}, {channel}, or {original_message}"
)
return
await self.config_manager.update_setting(
ctx.guild.id, "message_template", template
)
await self._handle_response(ctx, f"Message template has been set to: {template}")
except Exception as e:
logger.error(f"Error setting message template: {e}")
await self._handle_response(ctx, "An error occurred while setting the message template.")
@archiver.command(name="setconcurrent")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(count="Number of concurrent downloads (1-5)")
async def set_concurrent_downloads(self, ctx: Context, count: int):
"""Set the number of concurrent downloads allowed."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
if not 1 <= count <= 5:
await self._handle_response(ctx, "Concurrent downloads must be between 1 and 5.")
return
await self.config_manager.update_setting(
ctx.guild.id, "concurrent_downloads", count
)
await self._handle_response(ctx, f"Concurrent downloads has been set to {count}.")
except Exception as e:
logger.error(f"Error setting concurrent downloads: {e}")
await self._handle_response(ctx, "An error occurred while setting concurrent downloads.")
@archiver.command(name="settings")
@guild_only()
async def show_settings(self, ctx: Context):
"""Show current archiver settings."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
embed = await self.config_manager.format_settings_embed(ctx.guild)
# Use the helper method to handle the response
await self._handle_response(ctx, embed=embed)
except Exception as e:
logger.error(f"Error showing settings: {e}")
await self._handle_response(ctx, "An error occurred while showing settings.")
@archiver.command(name="queue")
@guild_only()
async def show_queue(self, ctx: Context):
"""Show the current video processing queue."""
# Defer the response immediately for slash commands
if hasattr(ctx, 'interaction') and ctx.interaction:
await ctx.defer()
await self.processor.show_queue_details(ctx)
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
"""Handle command errors"""
error_msg = None
try:
if isinstance(error, MissingPermissions):
error_msg = "❌ You don't have permission to use this command."
elif isinstance(error, BotMissingPermissions):
error_msg = "❌ I don't have the required permissions to do that."
elif isinstance(error, MissingRequiredArgument):
error_msg = f"❌ Missing required argument: {error.param.name}"
elif isinstance(error, BadArgument):
error_msg = f"❌ Invalid argument: {str(error)}"
elif isinstance(error, ConfigError):
error_msg = f"❌ Configuration error: {str(error)}"
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."
)
if error_msg:
await self._handle_response(ctx, error_msg)
except Exception as e:
logger.error(f"Error handling command error: {str(e)}")
try:
await self._handle_response(
ctx,
"❌ An error occurred while handling another error. Please check the logs."
)
except Exception:
pass
def _init_callback(self, task: asyncio.Task) -> None:
"""Handle initialization task completion"""
try:
task.result()
logger.info("Initialization completed successfully")
except asyncio.CancelledError:
logger.warning("Initialization was cancelled")
asyncio.create_task(self._cleanup())
except Exception as e:
logger.error(f"Initialization failed: {str(e)}\n{traceback.format_exc()}")
asyncio.create_task(self._cleanup())
async def _initialize(self) -> None:
"""Initialize all components with proper error handling"""
try:
# Initialize config first as other components depend on it
config = Config.get_conf(self, identifier=855847, force_registration=True)
config.register_guild(**self.default_guild_settings)
self.config_manager = ConfigManager(config)
logger.info("Config manager initialized")
# 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)
logger.info("Paths initialized")
# Clean existing downloads
try:
await cleanup_downloads(str(self.download_path))
except Exception as e:
logger.warning(f"Download cleanup error: {e}")
# Initialize shared FFmpeg manager
self.ffmpeg_mgr = FFmpegManager()
# Initialize queue manager
queue_path = self.data_path / "queue_state.json"
queue_path.parent.mkdir(parents=True, exist_ok=True)
self.queue_manager = EnhancedVideoQueueManager(
max_retries=3,
retry_delay=5,
max_queue_size=1000,
cleanup_interval=1800,
max_history_age=86400,
persistence_path=str(queue_path),
)
await self.queue_manager.initialize()
# Initialize processor
self.processor = VideoProcessor(
self.bot,
self.config_manager,
self.components,
queue_manager=self.queue_manager,
ffmpeg_mgr=self.ffmpeg_mgr,
db=self.db,
)
# Initialize components for existing guilds
for guild in self.bot.guilds:
try:
await initialize_guild_components(self, guild.id)
except Exception as e:
logger.error(f"Failed to initialize guild {guild.id}: {str(e)}")
continue
# Initialize update checker
self.update_checker = UpdateChecker(self.bot, self.config_manager)
await self.update_checker.start()
# Start queue processing as a background task
self._queue_task = asyncio.create_task(
self.queue_manager.process_queue(self.processor.process_video)
)
# Set ready flag
self.ready.set()
logger.info("VideoArchiver initialization completed successfully")
except Exception as e:
logger.error(f"Error during initialization: {str(e)}")
await self._cleanup()
raise
async def cog_load(self) -> None: async def cog_load(self) -> None:
"""Handle cog loading without blocking""" """Handle cog loading without blocking"""
try: try:
# Start initialization as background task without waiting # Start initialization as background task without waiting
self._init_task = asyncio.create_task(self._initialize()) self._init_task = asyncio.create_task(initialize_cog(self))
self._init_task.add_done_callback(self._init_callback) self._init_task.add_done_callback(lambda t: init_callback(self, t))
logger.info("Initialization started in background") logger.info("Initialization started in background")
except Exception as e: except Exception as e:
# Ensure cleanup on any error # Ensure cleanup on any error
@@ -744,17 +152,6 @@ class VideoArchiver(GroupCog):
if hasattr(self, "_queue_task"): if hasattr(self, "_queue_task"):
self._queue_task = None self._queue_task = None
async def _cleanup(self) -> None: async def cog_command_error(self, ctx, error):
"""Clean up all resources with proper handling""" """Handle command errors"""
try: await handle_command_error(ctx, error)
await asyncio.wait_for(cleanup_resources(self), timeout=CLEANUP_TIMEOUT)
logger.info("Cleanup completed successfully")
except asyncio.TimeoutError:
logger.warning("Cleanup timed out, forcing cleanup")
try:
await asyncio.wait_for(
force_cleanup_resources(self), timeout=CLEANUP_TIMEOUT
)
logger.info("Force cleanup completed")
except asyncio.TimeoutError:
logger.error("Force cleanup timed out")

View File

@@ -0,0 +1,11 @@
"""Command handlers for VideoArchiver"""
from .archiver_commands import setup_archiver_commands
from .database_commands import setup_database_commands
from .settings_commands import setup_settings_commands
__all__ = [
'setup_archiver_commands',
'setup_database_commands',
'setup_settings_commands'
]

View File

@@ -0,0 +1,82 @@
"""Module for core archiver commands"""
import discord
from redbot.core.commands import Context, hybrid_group, guild_only, checks
from discord import app_commands
import logging
from ..response_handler import handle_response
logger = logging.getLogger("VideoArchiver")
def setup_archiver_commands(cog):
"""Set up archiver commands for the cog"""
@cog.hybrid_group(name="archiver", fallback="help")
@guild_only()
async def archiver(ctx: Context):
"""Manage video archiver settings."""
if ctx.invoked_subcommand is None:
await handle_response(
ctx, "Use `/help archiver` for a list of commands."
)
@archiver.command(name="enable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def enable_archiver(ctx: Context):
"""Enable video archiving in this server."""
try:
current_setting = await cog.config_manager.get_setting(
ctx.guild.id, "enabled"
)
if current_setting:
await handle_response(ctx, "Video archiving is already enabled.")
return
await cog.config_manager.update_setting(ctx.guild.id, "enabled", True)
await handle_response(ctx, "Video archiving has been enabled.")
except Exception as e:
logger.error(f"Error enabling archiver: {e}")
await handle_response(
ctx, "An error occurred while enabling video archiving."
)
@archiver.command(name="disable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def disable_archiver(ctx: Context):
"""Disable video archiving in this server."""
try:
current_setting = await cog.config_manager.get_setting(
ctx.guild.id, "enabled"
)
if not current_setting:
await handle_response(ctx, "Video archiving is already disabled.")
return
await cog.config_manager.update_setting(ctx.guild.id, "enabled", False)
await handle_response(ctx, "Video archiving has been disabled.")
except Exception as e:
logger.error(f"Error disabling archiver: {e}")
await handle_response(
ctx, "An error occurred while disabling video archiving."
)
@archiver.command(name="queue")
@guild_only()
async def show_queue(ctx: Context):
"""Show the current video processing queue."""
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
await cog.processor.show_queue_details(ctx)
# Store commands in cog for access
cog.archiver = archiver
cog.enable_archiver = enable_archiver
cog.disable_archiver = disable_archiver
cog.show_queue = show_queue
return archiver

View File

@@ -0,0 +1,149 @@
"""Module for database-related commands"""
import discord
from redbot.core.commands import Context, hybrid_group, hybrid_command, guild_only, checks
from discord import app_commands
import logging
from ..response_handler import handle_response
from ...database.video_archive_db import VideoArchiveDB
logger = logging.getLogger("VideoArchiver")
def setup_database_commands(cog):
"""Set up database commands for the cog"""
@cog.hybrid_group(name="archivedb", fallback="help")
@guild_only()
async def archivedb(ctx: Context):
"""Manage the video archive database."""
if ctx.invoked_subcommand is None:
await handle_response(
ctx, "Use `/help archivedb` for a list of commands."
)
@archivedb.command(name="enable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def enable_database(ctx: Context):
"""Enable the video archive database."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
# Check if database is already enabled
current_setting = await cog.config_manager.get_setting(
ctx.guild.id, "use_database"
)
if current_setting:
await handle_response(
ctx, "The video archive database is already enabled."
)
return
# Initialize database
cog.db = VideoArchiveDB(cog.data_path)
# Update processor with database
if cog.processor:
cog.processor.db = cog.db
if cog.processor.queue_handler:
cog.processor.queue_handler.db = cog.db
# Update setting
await cog.config_manager.update_setting(ctx.guild.id, "use_database", True)
# Send success message
await handle_response(ctx, "Video archive database has been enabled.")
except Exception as e:
logger.error(f"Error enabling database: {e}")
await handle_response(
ctx,
"An error occurred while enabling the database. Please check the logs for details.",
)
@archivedb.command(name="disable")
@guild_only()
@checks.admin_or_permissions(administrator=True)
async def disable_database(ctx: Context):
"""Disable the video archive database."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
current_setting = await cog.config_manager.get_setting(
ctx.guild.id, "use_database"
)
if not current_setting:
await handle_response(
ctx, "The video archive database is already disabled."
)
return
# Remove database references
cog.db = None
cog.processor.db = None
cog.processor.queue_handler.db = None
await cog.config_manager.update_setting(
ctx.guild.id, "use_database", False
)
await handle_response(
ctx, "Video archive database has been disabled."
)
except Exception as e:
logger.error(f"Error disabling database: {e}")
await handle_response(
ctx, "An error occurred while disabling the database."
)
@cog.hybrid_command()
@guild_only()
@app_commands.describe(url="The URL of the video to check")
async def checkarchived(ctx: Context, url: str):
"""Check if a video URL has been archived and get its Discord link if it exists."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
if not cog.db:
await handle_response(
ctx,
"The archive database is not enabled. Ask an admin to enable it with `/archivedb enable`",
)
return
result = cog.db.get_archived_video(url)
if result:
discord_url, message_id, channel_id, guild_id = result
embed = discord.Embed(
title="Video Found in Archive",
description=f"This video has been archived!\n\nOriginal URL: {url}",
color=discord.Color.green(),
)
embed.add_field(name="Archived Link", value=discord_url)
await handle_response(ctx, embed=embed)
else:
embed = discord.Embed(
title="Video Not Found",
description="This video has not been archived yet.",
color=discord.Color.red(),
)
await handle_response(ctx, embed=embed)
except Exception as e:
logger.error(f"Error checking archived video: {e}")
await handle_response(
ctx, "An error occurred while checking the archive."
)
# Store commands in cog for access
cog.archivedb = archivedb
cog.enable_database = enable_database
cog.disable_database = disable_database
cog.checkarchived = checkarchived
return archivedb

View File

@@ -0,0 +1,330 @@
"""Module for settings-related commands"""
import discord
from redbot.core.commands import Context, hybrid_command, guild_only, checks
from discord import app_commands
import logging
from ..response_handler import handle_response
logger = logging.getLogger("VideoArchiver")
def setup_settings_commands(cog):
"""Set up settings commands for the cog"""
@cog.hybrid_command(name="setchannel")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel where archived videos will be stored")
async def set_archive_channel(ctx: Context, channel: discord.TextChannel):
"""Set the channel where archived videos will be stored."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
await cog.config_manager.update_setting(
ctx.guild.id, "archive_channel", channel.id
)
await handle_response(
ctx, f"Archive channel has been set to {channel.mention}."
)
except Exception as e:
logger.error(f"Error setting archive channel: {e}")
await handle_response(
ctx, "An error occurred while setting the archive channel."
)
@cog.hybrid_command(name="setlog")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel where log messages will be sent")
async def set_log_channel(ctx: Context, channel: discord.TextChannel):
"""Set the channel where log messages will be sent."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
await cog.config_manager.update_setting(
ctx.guild.id, "log_channel", channel.id
)
await handle_response(
ctx, f"Log channel has been set to {channel.mention}."
)
except Exception as e:
logger.error(f"Error setting log channel: {e}")
await handle_response(
ctx, "An error occurred while setting the log channel."
)
@cog.hybrid_command(name="addchannel")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel to monitor for videos")
async def add_enabled_channel(ctx: Context, channel: discord.TextChannel):
"""Add a channel to monitor for videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
enabled_channels = await cog.config_manager.get_setting(
ctx.guild.id, "enabled_channels"
)
if channel.id in enabled_channels:
await handle_response(
ctx, f"{channel.mention} is already being monitored."
)
return
enabled_channels.append(channel.id)
await cog.config_manager.update_setting(
ctx.guild.id, "enabled_channels", enabled_channels
)
await handle_response(
ctx, f"Now monitoring {channel.mention} for videos."
)
except Exception as e:
logger.error(f"Error adding enabled channel: {e}")
await handle_response(
ctx, "An error occurred while adding the channel."
)
@cog.hybrid_command(name="removechannel")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel to stop monitoring")
async def remove_enabled_channel(ctx: Context, channel: discord.TextChannel):
"""Remove a channel from video monitoring."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
enabled_channels = await cog.config_manager.get_setting(
ctx.guild.id, "enabled_channels"
)
if channel.id not in enabled_channels:
await handle_response(
ctx, f"{channel.mention} is not being monitored."
)
return
enabled_channels.remove(channel.id)
await cog.config_manager.update_setting(
ctx.guild.id, "enabled_channels", enabled_channels
)
await handle_response(
ctx, f"Stopped monitoring {channel.mention} for videos."
)
except Exception as e:
logger.error(f"Error removing enabled channel: {e}")
await handle_response(
ctx, "An error occurred while removing the channel."
)
@cog.hybrid_command(name="setformat")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(format="The video format to use (mp4, webm, or mkv)")
async def set_video_format(ctx: Context, format: str):
"""Set the video format for archived videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
format = format.lower()
if format not in ["mp4", "webm", "mkv"]:
await handle_response(
ctx, "Invalid format. Please use mp4, webm, or mkv."
)
return
await cog.config_manager.update_setting(
ctx.guild.id, "video_format", format
)
await handle_response(ctx, f"Video format has been set to {format}.")
except Exception as e:
logger.error(f"Error setting video format: {e}")
await handle_response(
ctx, "An error occurred while setting the video format."
)
@cog.hybrid_command(name="setquality")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(quality="The video quality (144-4320)")
async def set_video_quality(ctx: Context, quality: int):
"""Set the video quality for archived videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
if not 144 <= quality <= 4320:
await handle_response(
ctx, "Quality must be between 144 and 4320."
)
return
await cog.config_manager.update_setting(
ctx.guild.id, "video_quality", quality
)
await handle_response(
ctx, f"Video quality has been set to {quality}p."
)
except Exception as e:
logger.error(f"Error setting video quality: {e}")
await handle_response(
ctx, "An error occurred while setting the video quality."
)
@cog.hybrid_command(name="setmaxsize")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(size="The maximum file size in MB (1-100)")
async def set_max_file_size(ctx: Context, size: int):
"""Set the maximum file size for archived videos."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
if not 1 <= size <= 100:
await handle_response(ctx, "Size must be between 1 and 100 MB.")
return
await cog.config_manager.update_setting(
ctx.guild.id, "max_file_size", size
)
await handle_response(
ctx, f"Maximum file size has been set to {size}MB."
)
except Exception as e:
logger.error(f"Error setting max file size: {e}")
await handle_response(
ctx, "An error occurred while setting the maximum file size."
)
@cog.hybrid_command(name="setmessageduration")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(hours="How long to keep messages in hours (0-168)")
async def set_message_duration(ctx: Context, hours: int):
"""Set how long to keep archived messages."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
if not 0 <= hours <= 168:
await handle_response(
ctx, "Duration must be between 0 and 168 hours (1 week)."
)
return
await cog.config_manager.update_setting(
ctx.guild.id, "message_duration", hours
)
await handle_response(
ctx, f"Message duration has been set to {hours} hours."
)
except Exception as e:
logger.error(f"Error setting message duration: {e}")
await handle_response(
ctx, "An error occurred while setting the message duration."
)
@cog.hybrid_command(name="settemplate")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(
template="The message template to use. Available placeholders: {author}, {channel}, {original_message}"
)
async def set_message_template(ctx: Context, template: str):
"""Set the template for archived messages."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
if not any(
ph in template for ph in ["{author}", "{channel}", "{original_message}"]
):
await handle_response(
ctx,
"Template must include at least one placeholder: {author}, {channel}, or {original_message}",
)
return
await cog.config_manager.update_setting(
ctx.guild.id, "message_template", template
)
await handle_response(
ctx, f"Message template has been set to: {template}"
)
except Exception as e:
logger.error(f"Error setting message template: {e}")
await handle_response(
ctx, "An error occurred while setting the message template."
)
@cog.hybrid_command(name="setconcurrent")
@guild_only()
@checks.admin_or_permissions(administrator=True)
@app_commands.describe(count="Number of concurrent downloads (1-5)")
async def set_concurrent_downloads(ctx: Context, count: int):
"""Set the number of concurrent downloads allowed."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
if not 1 <= count <= 5:
await handle_response(
ctx, "Concurrent downloads must be between 1 and 5."
)
return
await cog.config_manager.update_setting(
ctx.guild.id, "concurrent_downloads", count
)
await handle_response(
ctx, f"Concurrent downloads has been set to {count}."
)
except Exception as e:
logger.error(f"Error setting concurrent downloads: {e}")
await handle_response(
ctx, "An error occurred while setting concurrent downloads."
)
@cog.hybrid_command(name="settings")
@guild_only()
async def show_settings(ctx: Context):
"""Show current archiver settings."""
try:
# Defer the response immediately for slash commands
if hasattr(ctx, "interaction") and ctx.interaction:
await ctx.defer()
embed = await cog.config_manager.format_settings_embed(ctx.guild)
await handle_response(ctx, embed=embed)
except Exception as e:
logger.error(f"Error showing settings: {e}")
await handle_response(
ctx, "An error occurred while showing settings."
)
# Store commands in cog for access
cog.set_archive_channel = set_archive_channel
cog.set_log_channel = set_log_channel
cog.add_enabled_channel = add_enabled_channel
cog.remove_enabled_channel = remove_enabled_channel
cog.set_video_format = set_video_format
cog.set_video_quality = set_video_quality
cog.set_max_file_size = set_max_file_size
cog.set_message_duration = set_message_duration
cog.set_message_template = set_message_template
cog.set_concurrent_downloads = set_concurrent_downloads
cog.show_settings = show_settings

View File

@@ -0,0 +1,46 @@
"""Module for handling command errors"""
import logging
import traceback
from redbot.core.commands import Context, MissingPermissions, BotMissingPermissions, MissingRequiredArgument, BadArgument
from ..utils.exceptions import VideoArchiverError as ProcessingError, ConfigurationError as ConfigError
from .response_handler import handle_response
logger = logging.getLogger("VideoArchiver")
async def handle_command_error(ctx: Context, error: Exception) -> None:
"""Handle command errors"""
error_msg = None
try:
if isinstance(error, MissingPermissions):
error_msg = "❌ You don't have permission to use this command."
elif isinstance(error, BotMissingPermissions):
error_msg = "❌ I don't have the required permissions to do that."
elif isinstance(error, MissingRequiredArgument):
error_msg = f"❌ Missing required argument: {error.param.name}"
elif isinstance(error, BadArgument):
error_msg = f"❌ Invalid argument: {str(error)}"
elif isinstance(error, ConfigError):
error_msg = f"❌ Configuration error: {str(error)}"
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."
)
if error_msg:
await handle_response(ctx, error_msg)
except Exception as e:
logger.error(f"Error handling command error: {str(e)}")
try:
await handle_response(
ctx,
"❌ An error occurred while handling another error. Please check the logs.",
)
except Exception:
pass

View File

@@ -0,0 +1,108 @@
"""Module for handling VideoArchiver initialization"""
import logging
import asyncio
import traceback
from pathlib import Path
from redbot.core import Config, data_manager
from ..config_manager import ConfigManager
from ..ffmpeg.ffmpeg_manager import FFmpegManager
from ..queue import EnhancedVideoQueueManager
from ..processor import VideoProcessor
from ..update_checker import UpdateChecker
from .guild import initialize_guild_components
from .cleanup import cleanup_downloads, cleanup_resources, force_cleanup_resources
from ..utils.exceptions import VideoArchiverError as ProcessingError
logger = logging.getLogger("VideoArchiver")
# Constants for timeouts
INIT_TIMEOUT = 60 # seconds
COMPONENT_INIT_TIMEOUT = 30 # seconds
CLEANUP_TIMEOUT = 15 # seconds
async def initialize_cog(cog) -> None:
"""Initialize all components with proper error handling"""
try:
# Initialize config first as other components depend on it
config = Config.get_conf(cog, identifier=855847, force_registration=True)
config.register_guild(**cog.default_guild_settings)
cog.config_manager = ConfigManager(config)
logger.info("Config manager initialized")
# Set up paths
cog.data_path = Path(data_manager.cog_data_path(cog))
cog.download_path = cog.data_path / "downloads"
cog.download_path.mkdir(parents=True, exist_ok=True)
logger.info("Paths initialized")
# Clean existing downloads
try:
await cleanup_downloads(str(cog.download_path))
except Exception as e:
logger.warning(f"Download cleanup error: {e}")
# Initialize shared FFmpeg manager
cog.ffmpeg_mgr = FFmpegManager()
# Initialize queue manager
queue_path = cog.data_path / "queue_state.json"
queue_path.parent.mkdir(parents=True, exist_ok=True)
cog.queue_manager = EnhancedVideoQueueManager(
max_retries=3,
retry_delay=5,
max_queue_size=1000,
cleanup_interval=1800,
max_history_age=86400,
persistence_path=str(queue_path),
)
await cog.queue_manager.initialize()
# Initialize processor
cog.processor = VideoProcessor(
cog.bot,
cog.config_manager,
cog.components,
queue_manager=cog.queue_manager,
ffmpeg_mgr=cog.ffmpeg_mgr,
db=cog.db,
)
# Initialize components for existing guilds
for guild in cog.bot.guilds:
try:
await initialize_guild_components(cog, guild.id)
except Exception as e:
logger.error(f"Failed to initialize guild {guild.id}: {str(e)}")
continue
# Initialize update checker
cog.update_checker = UpdateChecker(cog.bot, cog.config_manager)
await cog.update_checker.start()
# Start queue processing as a background task
cog._queue_task = asyncio.create_task(
cog.queue_manager.process_queue(cog.processor.process_video)
)
# Set ready flag
cog.ready.set()
logger.info("VideoArchiver initialization completed successfully")
except Exception as e:
logger.error(f"Error during initialization: {str(e)}")
await cleanup_resources(cog)
raise
def init_callback(cog, task: asyncio.Task) -> None:
"""Handle initialization task completion"""
try:
task.result()
logger.info("Initialization completed successfully")
except asyncio.CancelledError:
logger.warning("Initialization was cancelled")
asyncio.create_task(cleanup_resources(cog))
except Exception as e:
logger.error(f"Initialization failed: {str(e)}\n{traceback.format_exc()}")
asyncio.create_task(cleanup_resources(cog))

View File

@@ -0,0 +1,78 @@
"""Module for handling command responses"""
import logging
import discord
from redbot.core.commands import Context
logger = logging.getLogger("VideoArchiver")
async def handle_response(ctx: Context, content: str = None, embed: discord.Embed = None) -> None:
"""Helper method to handle responses for both regular commands and interactions"""
try:
# Check if this is a slash command interaction
is_interaction = hasattr(ctx, "interaction") and ctx.interaction is not None
if is_interaction:
try:
# For slash commands
if not ctx.interaction.response.is_done():
# If not responded yet, send initial response
if embed:
await ctx.interaction.response.send_message(
content=content, embed=embed
)
else:
await ctx.interaction.response.send_message(content=content)
else:
# If already responded (deferred), use followup
try:
if embed:
await ctx.interaction.followup.send(
content=content, embed=embed
)
else:
await ctx.interaction.followup.send(content=content)
except AttributeError:
# Fallback if followup is not available
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
except discord.errors.InteractionResponded:
# If interaction was already responded to, try followup
try:
if embed:
await ctx.interaction.followup.send(
content=content, embed=embed
)
else:
await ctx.interaction.followup.send(content=content)
except (AttributeError, discord.errors.HTTPException):
# Final fallback to regular message
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
except Exception as e:
logger.error(f"Error handling interaction response: {e}")
# Fallback to regular message
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
else:
# Regular command response
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
except Exception as e:
logger.error(f"Error sending response: {e}")
# Final fallback attempt
try:
if embed:
await ctx.send(content=content, embed=embed)
else:
await ctx.send(content=content)
except Exception as e2:
logger.error(f"Failed to send fallback message: {e2}")