loads of import fixes

This commit is contained in:
pacnpal
2024-11-17 19:47:18 +00:00
parent f71e174c0d
commit 97dd6d72f2
49 changed files with 1061 additions and 1062 deletions

View File

@@ -1,5 +1,5 @@
"""Core module for VideoArchiver cog"""
from .core.base import VideoArchiver
from ..core.base import VideoArchiver
__all__ = ["VideoArchiver"]

View File

@@ -8,9 +8,9 @@ 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
import discord # type: ignore
from redbot.core.bot import Red # type: ignore
from redbot.core.commands import GroupCog, Context # type: ignore
from .settings import Settings
from .lifecycle import LifecycleManager, LifecycleState
@@ -22,12 +22,12 @@ 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 (
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

View File

@@ -8,8 +8,8 @@ from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Any, Optional, TypedDict, ClassVar
from .utils.file_ops import cleanup_downloads
from .utils.exceptions import (
from ..utils.file_ops import cleanup_downloads
from ..utils.exceptions import (
CleanupError,
ErrorContext,
ErrorSeverity

View File

@@ -1,11 +1,11 @@
"""Command handlers for VideoArchiver"""
from .core.commands.archiver_commands import setup_archiver_commands
from .core.commands.database_commands import setup_database_commands
from .core.commands.settings_commands import setup_settings_commands
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'
"setup_archiver_commands",
"setup_database_commands",
"setup_settings_commands",
]

View File

@@ -4,47 +4,44 @@ import logging
from enum import Enum, auto
from typing import Optional, Any, Dict, TypedDict, Callable, Awaitable
import discord
from discord import app_commands
from redbot.core import commands
from redbot.core.commands import Context, hybrid_group, guild_only, admin_or_permissions
import discord # type: ignore
from discord import app_commands # type: ignore
from redbot.core import commands # type: ignore
from redbot.core.commands import Context, hybrid_group, guild_only, admin_or_permissions # type: ignore
from .core.response_handler import handle_response, ResponseType
from .utils.exceptions import (
CommandError,
ErrorContext,
ErrorSeverity
)
from core.response_handler import handle_response, ResponseType
from utils.exceptions import CommandError, ErrorContext, ErrorSeverity
logger = logging.getLogger("VideoArchiver")
class CommandCategory(Enum):
"""Command categories"""
MANAGEMENT = auto()
STATUS = auto()
UTILITY = auto()
class CommandResult(TypedDict):
"""Type definition for command result"""
success: bool
message: str
details: Optional[Dict[str, Any]]
error: Optional[str]
class CommandContext:
"""Context manager for command execution"""
def __init__(
self,
ctx: Context,
category: CommandCategory,
operation: str
) -> None:
def __init__(self, ctx: Context, category: CommandCategory, operation: str) -> None:
self.ctx = ctx
self.category = category
self.operation = operation
self.start_time = None
async def __aenter__(self) -> 'CommandContext':
async def __aenter__(self) -> "CommandContext":
"""Set up command context"""
self.start_time = self.ctx.message.created_at
logger.debug(
@@ -62,22 +59,23 @@ class CommandContext:
await handle_response(
self.ctx,
f"An error occurred: {str(exc_val)}",
response_type=ResponseType.ERROR
response_type=ResponseType.ERROR,
)
return True
return False
def setup_archiver_commands(cog: Any) -> Callable:
"""
Set up archiver commands for the cog.
Args:
cog: VideoArchiver cog instance
Returns:
Main archiver command group
"""
@hybrid_group(name="archiver", fallback="help")
@guild_only()
async def archiver(ctx: Context) -> None:
@@ -86,7 +84,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
await handle_response(
ctx,
"Use `/help archiver` for a list of commands.",
response_type=ResponseType.INFO
response_type=ResponseType.INFO,
)
@archiver.command(name="enable")
@@ -104,8 +102,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"enable_archiver",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Check current setting
@@ -116,7 +114,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
await handle_response(
ctx,
"Video archiving is already enabled.",
response_type=ResponseType.WARNING
response_type=ResponseType.WARNING,
)
return
@@ -125,7 +123,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
await handle_response(
ctx,
"Video archiving has been enabled.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -137,8 +135,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"enable_archiver",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@archiver.command(name="disable")
@@ -156,8 +154,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"disable_archiver",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Check current setting
@@ -168,7 +166,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
await handle_response(
ctx,
"Video archiving is already disabled.",
response_type=ResponseType.WARNING
response_type=ResponseType.WARNING,
)
return
@@ -177,7 +175,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
await handle_response(
ctx,
"Video archiving has been disabled.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -189,8 +187,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"disable_archiver",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@archiver.command(name="queue")
@@ -207,8 +205,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"show_queue",
{"guild_id": ctx.guild.id},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
await cog.processor.show_queue_details(ctx)
@@ -222,8 +220,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"show_queue",
{"guild_id": ctx.guild.id},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
@archiver.command(name="status")
@@ -235,22 +233,32 @@ def setup_archiver_commands(cog: Any) -> Callable:
try:
# Get comprehensive status
status = {
"enabled": await cog.config_manager.get_setting(ctx.guild.id, "enabled"),
"queue": cog.queue_manager.get_queue_status(ctx.guild.id) if cog.queue_manager else None,
"enabled": await cog.config_manager.get_setting(
ctx.guild.id, "enabled"
),
"queue": (
cog.queue_manager.get_queue_status(ctx.guild.id)
if cog.queue_manager
else None
),
"processor": cog.processor.get_status() if cog.processor else None,
"components": cog.component_manager.get_component_status(),
"health": cog.status_tracker.get_status()
"health": cog.status_tracker.get_status(),
}
# Create status embed
embed = discord.Embed(
title="VideoArchiver Status",
color=discord.Color.blue() if status["enabled"] else discord.Color.red()
color=(
discord.Color.blue()
if status["enabled"]
else discord.Color.red()
),
)
embed.add_field(
name="Status",
value="Enabled" if status["enabled"] else "Disabled",
inline=False
inline=False,
)
if status["queue"]:
@@ -261,7 +269,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
f"Processing: {status['queue']['processing']}\n"
f"Completed: {status['queue']['completed']}"
),
inline=True
inline=True,
)
if status["processor"]:
@@ -271,7 +279,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
f"Active: {status['processor']['active']}\n"
f"Health: {status['processor']['health']}"
),
inline=True
inline=True,
)
embed.add_field(
@@ -280,7 +288,7 @@ def setup_archiver_commands(cog: Any) -> Callable:
f"State: {status['health']['state']}\n"
f"Errors: {status['health']['error_count']}"
),
inline=True
inline=True,
)
await ctx.send(embed=embed)
@@ -294,8 +302,8 @@ def setup_archiver_commands(cog: Any) -> Callable:
"ArchiverCommands",
"show_status",
{"guild_id": ctx.guild.id},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
# Store commands in cog for access

View File

@@ -5,31 +5,30 @@ from datetime import datetime
from enum import Enum, auto
from typing import Optional, Any, Dict, TypedDict
import discord
from discord import app_commands
from redbot.core import commands
from redbot.core.commands import Context, hybrid_group, guild_only, admin_or_permissions
import discord # type: ignore
from discord import app_commands # type: ignore
from redbot.core import commands # type: ignore
from redbot.core.commands import Context, hybrid_group, guild_only, admin_or_permissions # type: ignore
from .core.response_handler import handle_response, ResponseType
from .utils.exceptions import (
CommandError,
ErrorContext,
ErrorSeverity,
DatabaseError
)
from .database.video_archive_db import VideoArchiveDB
from core.response_handler import handle_response, ResponseType
from utils.exceptions import CommandError, ErrorContext, ErrorSeverity, DatabaseError
from database.video_archive_db import VideoArchiveDB
logger = logging.getLogger("VideoArchiver")
class DatabaseOperation(Enum):
"""Database operation types"""
ENABLE = auto()
DISABLE = auto()
QUERY = auto()
MAINTENANCE = auto()
class DatabaseStatus(TypedDict):
"""Type definition for database status"""
enabled: bool
connected: bool
initialized: bool
@@ -37,8 +36,10 @@ class DatabaseStatus(TypedDict):
last_operation: Optional[str]
operation_time: Optional[str]
class ArchivedVideo(TypedDict):
"""Type definition for archived video data"""
url: str
discord_url: str
message_id: int
@@ -46,20 +47,23 @@ class ArchivedVideo(TypedDict):
guild_id: int
archived_at: str
async def check_database_status(cog: Any) -> DatabaseStatus:
"""
Check database status.
Args:
cog: VideoArchiver cog instance
Returns:
Database status information
"""
try:
enabled = await cog.config_manager.get_setting(
None, "use_database"
) if cog.config_manager else False
enabled = (
await cog.config_manager.get_setting(None, "use_database")
if cog.config_manager
else False
)
return DatabaseStatus(
enabled=enabled,
@@ -67,7 +71,7 @@ async def check_database_status(cog: Any) -> DatabaseStatus:
initialized=cog.db is not None,
error=None,
last_operation=None,
operation_time=datetime.utcnow().isoformat()
operation_time=datetime.utcnow().isoformat(),
)
except Exception as e:
return DatabaseStatus(
@@ -76,16 +80,17 @@ async def check_database_status(cog: Any) -> DatabaseStatus:
initialized=False,
error=str(e),
last_operation=None,
operation_time=datetime.utcnow().isoformat()
operation_time=datetime.utcnow().isoformat(),
)
def setup_database_commands(cog: Any) -> Any:
"""
Set up database commands for the cog.
Args:
cog: VideoArchiver cog instance
Returns:
Main database command group
"""
@@ -98,39 +103,39 @@ def setup_database_commands(cog: Any) -> Any:
try:
# Get database status
status = await check_database_status(cog)
# Create status embed
embed = discord.Embed(
title="Video Archive Database Status",
color=discord.Color.blue() if status["enabled"] else discord.Color.red()
color=(
discord.Color.blue()
if status["enabled"]
else discord.Color.red()
),
)
embed.add_field(
name="Status",
value="Enabled" if status["enabled"] else "Disabled",
inline=False
inline=False,
)
embed.add_field(
name="Connection",
value="Connected" if status["connected"] else "Disconnected",
inline=True
inline=True,
)
embed.add_field(
name="Initialization",
value="Initialized" if status["initialized"] else "Not Initialized",
inline=True
inline=True,
)
if status["error"]:
embed.add_field(
name="Error",
value=status["error"],
inline=False
)
embed.add_field(name="Error", value=status["error"], inline=False)
await handle_response(
ctx,
"Use `/help archivedb` for a list of commands.",
embed=embed,
response_type=ResponseType.INFO
response_type=ResponseType.INFO,
)
except Exception as e:
error = f"Failed to get database status: {str(e)}"
@@ -141,8 +146,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"show_status",
{"guild_id": ctx.guild.id},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
@archivedb.command(name="enable")
@@ -159,8 +164,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"enable_database",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -175,7 +180,7 @@ def setup_database_commands(cog: Any) -> Any:
await handle_response(
ctx,
"The video archive database is already enabled.",
response_type=ResponseType.WARNING
response_type=ResponseType.WARNING,
)
return
@@ -190,8 +195,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"enable_database",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Update processor with database
@@ -201,17 +206,13 @@ def setup_database_commands(cog: Any) -> Any:
cog.processor.queue_handler.db = cog.db
# Update setting
await cog.config_manager.update_setting(
ctx.guild.id,
"use_database",
True
)
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.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -223,8 +224,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"enable_database",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@archivedb.command(name="disable")
@@ -241,8 +242,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"disable_database",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -250,14 +251,13 @@ def setup_database_commands(cog: Any) -> Any:
await ctx.defer()
current_setting = await cog.config_manager.get_setting(
ctx.guild.id,
"use_database"
ctx.guild.id, "use_database"
)
if not current_setting:
await handle_response(
ctx,
"The video archive database is already disabled.",
response_type=ResponseType.WARNING
response_type=ResponseType.WARNING,
)
return
@@ -275,15 +275,11 @@ def setup_database_commands(cog: Any) -> Any:
if cog.processor.queue_handler:
cog.processor.queue_handler.db = None
await cog.config_manager.update_setting(
ctx.guild.id,
"use_database",
False
)
await cog.config_manager.update_setting(ctx.guild.id, "use_database", False)
await handle_response(
ctx,
"Video archive database has been disabled.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -295,8 +291,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"disable_database",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@archivedb.command(name="check")
@@ -310,7 +306,7 @@ def setup_database_commands(cog: Any) -> Any:
await handle_response(
ctx,
"The archive database is not enabled. Ask an admin to enable it with `/archivedb enable`",
response_type=ResponseType.ERROR
response_type=ResponseType.ERROR,
)
return
@@ -327,8 +323,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"checkarchived",
{"guild_id": ctx.guild.id, "url": url},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
if result:
@@ -338,30 +334,12 @@ def setup_database_commands(cog: Any) -> Any:
description=f"This video has been archived!\n\nOriginal URL: {url}",
color=discord.Color.green(),
)
embed.add_field(
name="Archived Link",
value=discord_url,
inline=False
)
embed.add_field(
name="Message ID",
value=str(message_id),
inline=True
)
embed.add_field(
name="Channel ID",
value=str(channel_id),
inline=True
)
embed.add_field(
name="Guild ID",
value=str(guild_id),
inline=True
)
embed.add_field(name="Archived Link", value=discord_url, inline=False)
embed.add_field(name="Message ID", value=str(message_id), inline=True)
embed.add_field(name="Channel ID", value=str(channel_id), inline=True)
embed.add_field(name="Guild ID", value=str(guild_id), inline=True)
await handle_response(
ctx,
embed=embed,
response_type=ResponseType.SUCCESS
ctx, embed=embed, response_type=ResponseType.SUCCESS
)
else:
embed = discord.Embed(
@@ -370,9 +348,7 @@ def setup_database_commands(cog: Any) -> Any:
color=discord.Color.red(),
)
await handle_response(
ctx,
embed=embed,
response_type=ResponseType.WARNING
ctx, embed=embed, response_type=ResponseType.WARNING
)
except Exception as e:
@@ -384,8 +360,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"checkarchived",
{"guild_id": ctx.guild.id, "url": url},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
@archivedb.command(name="status")
@@ -399,7 +375,7 @@ def setup_database_commands(cog: Any) -> Any:
await ctx.defer()
status = await check_database_status(cog)
# Get additional stats if database is enabled
stats = {}
if cog.db and status["connected"]:
@@ -410,53 +386,49 @@ def setup_database_commands(cog: Any) -> Any:
embed = discord.Embed(
title="Database Status",
color=discord.Color.green() if status["connected"] else discord.Color.red()
color=(
discord.Color.green()
if status["connected"]
else discord.Color.red()
),
)
embed.add_field(
name="Status",
value="Enabled" if status["enabled"] else "Disabled",
inline=False
inline=False,
)
embed.add_field(
name="Connection",
value="Connected" if status["connected"] else "Disconnected",
inline=True
inline=True,
)
embed.add_field(
name="Initialization",
value="Initialized" if status["initialized"] else "Not Initialized",
inline=True
inline=True,
)
if stats:
embed.add_field(
name="Total Videos",
value=str(stats.get("total_videos", 0)),
inline=True
inline=True,
)
embed.add_field(
name="Total Size",
value=f"{stats.get('total_size', 0)} MB",
inline=True
inline=True,
)
embed.add_field(
name="Last Update",
value=stats.get("last_update", "Never"),
inline=True
inline=True,
)
if status["error"]:
embed.add_field(
name="Error",
value=status["error"],
inline=False
)
embed.add_field(name="Error", value=status["error"], inline=False)
await handle_response(
ctx,
embed=embed,
response_type=ResponseType.INFO
)
await handle_response(ctx, embed=embed, response_type=ResponseType.INFO)
except Exception as e:
error = f"Failed to get database status: {str(e)}"
@@ -467,8 +439,8 @@ def setup_database_commands(cog: Any) -> Any:
"DatabaseCommands",
"database_status",
{"guild_id": ctx.guild.id},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
# Store commands in cog for access

View File

@@ -4,122 +4,130 @@ import logging
from enum import Enum, auto
from typing import Optional, Any, Dict, TypedDict
import discord
from discord import app_commands
from redbot.core import commands
from redbot.core.commands import Context, hybrid_group, guild_only, admin_or_permissions
import discord # type: ignore
from discord import app_commands # type: ignore
from redbot.core import commands # type: ignore
from redbot.core.commands import Context, hybrid_group, guild_only, admin_or_permissions # type: ignore
from .core.settings import VideoFormat, VideoQuality
from .core.response_handler import handle_response, ResponseType
from .utils.exceptions import (
CommandError,
ErrorContext,
ErrorSeverity
)
from core.settings import VideoFormat, VideoQuality
from core.response_handler import handle_response, ResponseType
from utils.exceptions import CommandError, ErrorContext, ErrorSeverity
logger = logging.getLogger("VideoArchiver")
class SettingCategory(Enum):
"""Setting categories"""
CHANNELS = auto()
VIDEO = auto()
MESSAGES = auto()
PERFORMANCE = auto()
class SettingValidation(TypedDict):
"""Type definition for setting validation"""
valid: bool
error: Optional[str]
details: Dict[str, Any]
class SettingUpdate(TypedDict):
"""Type definition for setting update"""
setting: str
old_value: Any
new_value: Any
category: SettingCategory
async def validate_setting(
category: SettingCategory,
setting: str,
value: Any
category: SettingCategory, setting: str, value: Any
) -> SettingValidation:
"""
Validate a setting value.
Args:
category: Setting category
setting: Setting name
value: Value to validate
Returns:
Validation result
"""
validation = SettingValidation(
valid=True,
error=None,
details={"category": category.name, "setting": setting, "value": value}
details={"category": category.name, "setting": setting, "value": value},
)
try:
if category == SettingCategory.VIDEO:
if setting == "format":
if value not in [f.value for f in VideoFormat]:
validation.update({
"valid": False,
"error": f"Invalid format. Must be one of: {', '.join(f.value for f in VideoFormat)}"
})
validation.update(
{
"valid": False,
"error": f"Invalid format. Must be one of: {', '.join(f.value for f in VideoFormat)}",
}
)
elif setting == "quality":
if not 144 <= value <= 4320:
validation.update({
"valid": False,
"error": "Quality must be between 144 and 4320"
})
validation.update(
{
"valid": False,
"error": "Quality must be between 144 and 4320",
}
)
elif setting == "max_file_size":
if not 1 <= value <= 100:
validation.update({
"valid": False,
"error": "Size must be between 1 and 100 MB"
})
validation.update(
{"valid": False, "error": "Size must be between 1 and 100 MB"}
)
elif category == SettingCategory.MESSAGES:
if setting == "duration":
if not 0 <= value <= 168:
validation.update({
"valid": False,
"error": "Duration must be between 0 and 168 hours (1 week)"
})
validation.update(
{
"valid": False,
"error": "Duration must be between 0 and 168 hours (1 week)",
}
)
elif setting == "template":
placeholders = ["{author}", "{channel}", "{original_message}"]
if not any(ph in value for ph in placeholders):
validation.update({
"valid": False,
"error": f"Template must include at least one placeholder: {', '.join(placeholders)}"
})
validation.update(
{
"valid": False,
"error": f"Template must include at least one placeholder: {', '.join(placeholders)}",
}
)
elif category == SettingCategory.PERFORMANCE:
if setting == "concurrent_downloads":
if not 1 <= value <= 5:
validation.update({
"valid": False,
"error": "Concurrent downloads must be between 1 and 5"
})
validation.update(
{
"valid": False,
"error": "Concurrent downloads must be between 1 and 5",
}
)
except Exception as e:
validation.update({
"valid": False,
"error": f"Validation error: {str(e)}"
})
validation.update({"valid": False, "error": f"Validation error: {str(e)}"})
return validation
def setup_settings_commands(cog: Any) -> Any:
"""
Set up settings commands for the cog.
Args:
cog: VideoArchiver cog instance
Returns:
Main settings command group
"""
@@ -137,8 +145,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"show_settings",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -146,11 +154,7 @@ def setup_settings_commands(cog: Any) -> Any:
await ctx.defer()
embed = await cog.config_manager.format_settings_embed(ctx.guild)
await handle_response(
ctx,
embed=embed,
response_type=ResponseType.INFO
)
await handle_response(ctx, embed=embed, response_type=ResponseType.INFO)
except Exception as e:
error = f"Failed to show settings: {str(e)}"
@@ -161,8 +165,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"show_settings",
{"guild_id": ctx.guild.id},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
@settings.command(name="setchannel")
@@ -180,8 +184,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_archive_channel",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -194,7 +198,7 @@ def setup_settings_commands(cog: Any) -> Any:
send_messages=True,
embed_links=True,
attach_files=True,
read_message_history=True
read_message_history=True,
)
channel_perms = channel.permissions_for(bot_member)
if not all(getattr(channel_perms, perm) for perm in required_perms):
@@ -207,23 +211,22 @@ def setup_settings_commands(cog: Any) -> Any:
"guild_id": ctx.guild.id,
"channel_id": channel.id,
"missing_perms": [
perm for perm in required_perms
perm
for perm in required_perms
if not getattr(channel_perms, perm)
]
],
},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
await cog.config_manager.update_setting(
ctx.guild.id,
"archive_channel",
channel.id
ctx.guild.id, "archive_channel", channel.id
)
await handle_response(
ctx,
f"Archive channel has been set to {channel.mention}.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -235,8 +238,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_archive_channel",
{"guild_id": ctx.guild.id, "channel_id": channel.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="setlog")
@@ -254,8 +257,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_log_channel",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -265,9 +268,7 @@ def setup_settings_commands(cog: Any) -> Any:
# Check channel permissions
bot_member = ctx.guild.me
required_perms = discord.Permissions(
send_messages=True,
embed_links=True,
read_message_history=True
send_messages=True, embed_links=True, read_message_history=True
)
channel_perms = channel.permissions_for(bot_member)
if not all(getattr(channel_perms, perm) for perm in required_perms):
@@ -280,23 +281,22 @@ def setup_settings_commands(cog: Any) -> Any:
"guild_id": ctx.guild.id,
"channel_id": channel.id,
"missing_perms": [
perm for perm in required_perms
perm
for perm in required_perms
if not getattr(channel_perms, perm)
]
],
},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
await cog.config_manager.update_setting(
ctx.guild.id,
"log_channel",
channel.id
ctx.guild.id, "log_channel", channel.id
)
await handle_response(
ctx,
f"Log channel has been set to {channel.mention}.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -308,8 +308,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_log_channel",
{"guild_id": ctx.guild.id, "channel_id": channel.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="addchannel")
@@ -327,8 +327,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"add_enabled_channel",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -338,8 +338,7 @@ def setup_settings_commands(cog: Any) -> Any:
# Check channel permissions
bot_member = ctx.guild.me
required_perms = discord.Permissions(
read_messages=True,
read_message_history=True
read_messages=True, read_message_history=True
)
channel_perms = channel.permissions_for(bot_member)
if not all(getattr(channel_perms, perm) for perm in required_perms):
@@ -352,36 +351,34 @@ def setup_settings_commands(cog: Any) -> Any:
"guild_id": ctx.guild.id,
"channel_id": channel.id,
"missing_perms": [
perm for perm in required_perms
perm
for perm in required_perms
if not getattr(channel_perms, perm)
]
],
},
ErrorSeverity.MEDIUM
)
ErrorSeverity.MEDIUM,
),
)
enabled_channels = await cog.config_manager.get_setting(
ctx.guild.id,
"enabled_channels"
ctx.guild.id, "enabled_channels"
)
if channel.id in enabled_channels:
await handle_response(
ctx,
f"{channel.mention} is already being monitored.",
response_type=ResponseType.WARNING
response_type=ResponseType.WARNING,
)
return
enabled_channels.append(channel.id)
await cog.config_manager.update_setting(
ctx.guild.id,
"enabled_channels",
enabled_channels
ctx.guild.id, "enabled_channels", enabled_channels
)
await handle_response(
ctx,
f"Now monitoring {channel.mention} for videos.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -393,15 +390,17 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"add_enabled_channel",
{"guild_id": ctx.guild.id, "channel_id": channel.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="removechannel")
@guild_only()
@admin_or_permissions(administrator=True)
@app_commands.describe(channel="The channel to stop monitoring")
async def remove_enabled_channel(ctx: Context, channel: discord.TextChannel) -> None:
async def remove_enabled_channel(
ctx: Context, channel: discord.TextChannel
) -> None:
"""Remove a channel from video monitoring."""
try:
# Check if config manager is ready
@@ -412,8 +411,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"remove_enabled_channel",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -421,27 +420,24 @@ def setup_settings_commands(cog: Any) -> Any:
await ctx.defer()
enabled_channels = await cog.config_manager.get_setting(
ctx.guild.id,
"enabled_channels"
ctx.guild.id, "enabled_channels"
)
if channel.id not in enabled_channels:
await handle_response(
ctx,
f"{channel.mention} is not being monitored.",
response_type=ResponseType.WARNING
response_type=ResponseType.WARNING,
)
return
enabled_channels.remove(channel.id)
await cog.config_manager.update_setting(
ctx.guild.id,
"enabled_channels",
enabled_channels
ctx.guild.id, "enabled_channels", enabled_channels
)
await handle_response(
ctx,
f"Stopped monitoring {channel.mention} for videos.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -453,8 +449,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"remove_enabled_channel",
{"guild_id": ctx.guild.id, "channel_id": channel.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="setformat")
@@ -472,8 +468,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_video_format",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -482,28 +478,20 @@ def setup_settings_commands(cog: Any) -> Any:
# Validate format
format = format.lower()
validation = await validate_setting(
SettingCategory.VIDEO,
"format",
format
)
validation = await validate_setting(SettingCategory.VIDEO, "format", format)
if not validation["valid"]:
await handle_response(
ctx,
validation["error"],
response_type=ResponseType.ERROR
ctx, validation["error"], response_type=ResponseType.ERROR
)
return
await cog.config_manager.update_setting(
ctx.guild.id,
"video_format",
format
ctx.guild.id, "video_format", format
)
await handle_response(
ctx,
f"Video format has been set to {format}.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -515,8 +503,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_video_format",
{"guild_id": ctx.guild.id, "format": format},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="setquality")
@@ -534,8 +522,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_video_quality",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -544,27 +532,21 @@ def setup_settings_commands(cog: Any) -> Any:
# Validate quality
validation = await validate_setting(
SettingCategory.VIDEO,
"quality",
quality
SettingCategory.VIDEO, "quality", quality
)
if not validation["valid"]:
await handle_response(
ctx,
validation["error"],
response_type=ResponseType.ERROR
ctx, validation["error"], response_type=ResponseType.ERROR
)
return
await cog.config_manager.update_setting(
ctx.guild.id,
"video_quality",
quality
ctx.guild.id, "video_quality", quality
)
await handle_response(
ctx,
f"Video quality has been set to {quality}p.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -576,8 +558,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_video_quality",
{"guild_id": ctx.guild.id, "quality": quality},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="setmaxsize")
@@ -595,8 +577,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_max_file_size",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -605,27 +587,19 @@ def setup_settings_commands(cog: Any) -> Any:
# Validate size
validation = await validate_setting(
SettingCategory.VIDEO,
"max_file_size",
size
SettingCategory.VIDEO, "max_file_size", size
)
if not validation["valid"]:
await handle_response(
ctx,
validation["error"],
response_type=ResponseType.ERROR
ctx, validation["error"], response_type=ResponseType.ERROR
)
return
await cog.config_manager.update_setting(
ctx.guild.id,
"max_file_size",
size
)
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.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -637,8 +611,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_max_file_size",
{"guild_id": ctx.guild.id, "size": size},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="setmessageduration")
@@ -656,8 +630,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_message_duration",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -666,27 +640,21 @@ def setup_settings_commands(cog: Any) -> Any:
# Validate duration
validation = await validate_setting(
SettingCategory.MESSAGES,
"duration",
hours
SettingCategory.MESSAGES, "duration", hours
)
if not validation["valid"]:
await handle_response(
ctx,
validation["error"],
response_type=ResponseType.ERROR
ctx, validation["error"], response_type=ResponseType.ERROR
)
return
await cog.config_manager.update_setting(
ctx.guild.id,
"message_duration",
hours
ctx.guild.id, "message_duration", hours
)
await handle_response(
ctx,
f"Message duration has been set to {hours} hours.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -698,8 +666,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_message_duration",
{"guild_id": ctx.guild.id, "hours": hours},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="settemplate")
@@ -719,8 +687,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_message_template",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -729,27 +697,21 @@ def setup_settings_commands(cog: Any) -> Any:
# Validate template
validation = await validate_setting(
SettingCategory.MESSAGES,
"template",
template
SettingCategory.MESSAGES, "template", template
)
if not validation["valid"]:
await handle_response(
ctx,
validation["error"],
response_type=ResponseType.ERROR
ctx, validation["error"], response_type=ResponseType.ERROR
)
return
await cog.config_manager.update_setting(
ctx.guild.id,
"message_template",
template
ctx.guild.id, "message_template", template
)
await handle_response(
ctx,
f"Message template has been set to: {template}",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -761,8 +723,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_message_template",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
@settings.command(name="setconcurrent")
@@ -780,8 +742,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_concurrent_downloads",
{"guild_id": ctx.guild.id},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Defer the response immediately for slash commands
@@ -790,27 +752,21 @@ def setup_settings_commands(cog: Any) -> Any:
# Validate count
validation = await validate_setting(
SettingCategory.PERFORMANCE,
"concurrent_downloads",
count
SettingCategory.PERFORMANCE, "concurrent_downloads", count
)
if not validation["valid"]:
await handle_response(
ctx,
validation["error"],
response_type=ResponseType.ERROR
ctx, validation["error"], response_type=ResponseType.ERROR
)
return
await cog.config_manager.update_setting(
ctx.guild.id,
"concurrent_downloads",
count
ctx.guild.id, "concurrent_downloads", count
)
await handle_response(
ctx,
f"Concurrent downloads has been set to {count}.",
response_type=ResponseType.SUCCESS
response_type=ResponseType.SUCCESS,
)
except Exception as e:
@@ -822,8 +778,8 @@ def setup_settings_commands(cog: Any) -> Any:
"SettingsCommands",
"set_concurrent_downloads",
{"guild_id": ctx.guild.id, "count": count},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
# Store commands in cog for access

View File

@@ -8,6 +8,7 @@ from typing import (
Optional,
Set,
List,
Tuple,
TypedDict,
ClassVar,
Type,
@@ -19,12 +20,12 @@ from datetime import datetime
from pathlib import Path
import importlib
from .utils.exceptions import ComponentError, ErrorContext, ErrorSeverity
from .utils.path_manager import ensure_directory
from .config_manager import ConfigManager
from .processor.core import Processor
from .queue.manager import EnhancedVideoQueueManager
from .ffmpeg.ffmpeg_manager import FFmpegManager
from ..utils.exceptions import ComponentError, ErrorContext, ErrorSeverity
from ..utils.path_manager import ensure_directory
from ..config_manager import ConfigManager
from ..processor.core import Processor
from ..queue.manager import EnhancedVideoQueueManager
from ..ffmpeg.ffmpeg_manager import FFmpegManager
logger = logging.getLogger("VideoArchiver")

View File

@@ -4,8 +4,8 @@ import logging
import traceback
from typing import Dict, Optional, Tuple, Type, TypedDict, ClassVar
from enum import Enum, auto
import discord
from redbot.core.commands import (
import discord # type: ignore
from redbot.core.commands import ( # type: ignore
Context,
MissingPermissions,
BotMissingPermissions,
@@ -14,7 +14,7 @@ from redbot.core.commands import (
CommandError
)
from .utils.exceptions import (
from ..utils.exceptions import (
VideoArchiverError,
ErrorSeverity,
ErrorContext,
@@ -33,7 +33,7 @@ from .utils.exceptions import (
ResourceExhaustedError,
ConfigurationError
)
from .core.response_handler import response_manager
from ..core.response_handler import response_manager
logger = logging.getLogger("VideoArchiver")

View File

@@ -7,23 +7,24 @@ from datetime import datetime
from enum import Enum, auto
from typing import TYPE_CHECKING, Dict, Any, Optional, TypedDict, ClassVar, List
import discord
import discord # type: ignore
from .processor.constants import REACTIONS
from .processor.reactions import handle_archived_reaction
from .core.guild import initialize_guild_components, cleanup_guild_components
from .core.error_handler import error_manager
from .core.response_handler import response_manager
from .utils.exceptions import EventError, ErrorContext, ErrorSeverity
from ..processor.constants import REACTIONS
from ..processor.reactions import handle_archived_reaction
from ..core.guild import initialize_guild_components, cleanup_guild_components
from ..core.error_handler import error_manager
from ..core.response_handler import response_manager
from ..utils.exceptions import EventError, ErrorContext, ErrorSeverity
if TYPE_CHECKING:
from .core.base import VideoArchiver
from ..core.base import VideoArchiver
logger = logging.getLogger("VideoArchiver")
class EventType(Enum):
"""Types of Discord events"""
GUILD_JOIN = auto()
GUILD_REMOVE = auto()
MESSAGE = auto()
@@ -34,6 +35,7 @@ class EventType(Enum):
class EventStats(TypedDict):
"""Type definition for event statistics"""
counts: Dict[str, int]
last_events: Dict[str, str]
errors: Dict[str, int]
@@ -43,6 +45,7 @@ class EventStats(TypedDict):
class EventHistory(TypedDict):
"""Type definition for event history entry"""
event_type: str
timestamp: str
guild_id: Optional[int]
@@ -94,7 +97,7 @@ class EventTracker:
# Cleanup old history
if len(self.history) > self.MAX_HISTORY:
self.history = self.history[-self.MAX_HISTORY:]
self.history = self.history[-self.MAX_HISTORY :]
def record_error(
self, event_type: EventType, error: str, duration: float = 0.0

View File

@@ -4,16 +4,17 @@ import logging
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Any, Optional
from .utils.download_core import DownloadCore
from .utils.message_manager import MessageManager
from .utils.file_ops import cleanup_downloads
from .utils.exceptions import VideoArchiverError as ProcessingError
from ..utils.download_core import DownloadCore
from ..utils.message_manager import MessageManager
from ..utils.file_ops import cleanup_downloads
from ..utils.exceptions import VideoArchiverError as ProcessingError
if TYPE_CHECKING:
from .core.base import VideoArchiver
from ..core.base import VideoArchiver
logger = logging.getLogger("VideoArchiver")
async def initialize_guild_components(cog: "VideoArchiver", guild_id: int) -> None:
"""Initialize or update components for a guild with error handling"""
try:
@@ -53,6 +54,7 @@ async def initialize_guild_components(cog: "VideoArchiver", guild_id: int) -> No
logger.error(f"Failed to initialize guild {guild_id}: {str(e)}")
raise ProcessingError(f"Guild initialization failed: {str(e)}")
async def cleanup_guild_components(cog: "VideoArchiver", guild_id: int) -> None:
"""Clean up components for a specific guild"""
try:

View File

@@ -4,15 +4,15 @@ from typing import TYPE_CHECKING, Optional, Dict, Any
import asyncio
import logging
from .utils.exceptions import (
from ..utils.exceptions import (
ComponentError,
ErrorContext,
ErrorSeverity
)
from .core.lifecycle import LifecycleState
from ..core.lifecycle import LifecycleState
if TYPE_CHECKING:
from .core.base import VideoArchiver
from ..core.base import VideoArchiver
logger = logging.getLogger("VideoArchiver")

View File

@@ -7,54 +7,65 @@ from typing import Optional, Dict, Any, Set, List, Callable, TypedDict, ClassVar
from enum import Enum, auto
from datetime import datetime
from .core.cleanup import cleanup_resources, force_cleanup_resources
from .utils.exceptions import (
from ..core.cleanup import cleanup_resources, force_cleanup_resources
from ..utils.exceptions import (
VideoArchiverError,
ErrorContext,
ErrorSeverity,
ComponentError,
CleanupError
CleanupError,
)
logger = logging.getLogger("VideoArchiver")
class LifecycleState(Enum):
"""Possible states in the cog lifecycle"""
UNINITIALIZED = auto()
INITIALIZING = auto()
READY = auto()
UNLOADING = auto()
ERROR = auto()
class TaskStatus(Enum):
"""Task execution status"""
RUNNING = auto()
COMPLETED = auto()
CANCELLED = auto()
FAILED = auto()
class TaskHistory(TypedDict):
"""Type definition for task history entry"""
start_time: str
end_time: Optional[str]
status: str
error: Optional[str]
duration: float
class StateHistory(TypedDict):
"""Type definition for state history entry"""
state: str
timestamp: str
duration: float
details: Optional[Dict[str, Any]]
class LifecycleStatus(TypedDict):
"""Type definition for lifecycle status"""
state: str
state_history: List[StateHistory]
tasks: Dict[str, Any]
health: bool
class TaskManager:
"""Manages asyncio tasks"""
@@ -69,20 +80,20 @@ class TaskManager:
name: str,
coro: Callable[..., Any],
callback: Optional[Callable[[asyncio.Task], None]] = None,
timeout: Optional[float] = None
timeout: Optional[float] = None,
) -> asyncio.Task:
"""
Create and track a task.
Args:
name: Task name
coro: Coroutine to run
callback: Optional completion callback
timeout: Optional timeout in seconds
Returns:
Created task
Raises:
ComponentError: If task creation fails
"""
@@ -94,14 +105,16 @@ class TaskManager:
end_time=None,
status=TaskStatus.RUNNING.name,
error=None,
duration=0.0
duration=0.0,
)
if timeout:
asyncio.create_task(self._handle_timeout(name, task, timeout))
if callback:
task.add_done_callback(lambda t: self._handle_completion(name, t, callback))
task.add_done_callback(
lambda t: self._handle_completion(name, t, callback)
)
else:
task.add_done_callback(lambda t: self._handle_completion(name, t))
@@ -116,15 +129,12 @@ class TaskManager:
"TaskManager",
"create_task",
{"task_name": name},
ErrorSeverity.HIGH
)
ErrorSeverity.HIGH,
),
)
async def _handle_timeout(
self,
name: str,
task: asyncio.Task,
timeout: float
self, name: str, task: asyncio.Task, timeout: float
) -> None:
"""Handle task timeout"""
try:
@@ -134,16 +144,14 @@ class TaskManager:
logger.warning(f"Task {name} timed out after {timeout}s")
task.cancel()
self._update_task_history(
name,
TaskStatus.FAILED,
f"Task timed out after {timeout}s"
name, TaskStatus.FAILED, f"Task timed out after {timeout}s"
)
def _handle_completion(
self,
name: str,
task: asyncio.Task,
callback: Optional[Callable[[asyncio.Task], None]] = None
callback: Optional[Callable[[asyncio.Task], None]] = None,
) -> None:
"""Handle task completion"""
try:
@@ -169,26 +177,25 @@ class TaskManager:
self._tasks.pop(name, None)
def _update_task_history(
self,
name: str,
status: TaskStatus,
error: Optional[str] = None
self, name: str, status: TaskStatus, error: Optional[str] = None
) -> None:
"""Update task history entry"""
if name in self._task_history:
end_time = datetime.utcnow()
start_time = datetime.fromisoformat(self._task_history[name]["start_time"])
self._task_history[name].update({
"end_time": end_time.isoformat(),
"status": status.name,
"error": error,
"duration": (end_time - start_time).total_seconds()
})
self._task_history[name].update(
{
"end_time": end_time.isoformat(),
"status": status.name,
"error": error,
"duration": (end_time - start_time).total_seconds(),
}
)
async def cancel_task(self, name: str) -> None:
"""
Cancel a specific task.
Args:
name: Task name to cancel
"""
@@ -210,15 +217,16 @@ class TaskManager:
def get_task_status(self) -> Dict[str, Any]:
"""
Get status of all tasks.
Returns:
Dictionary containing task status information
"""
return {
"active_tasks": list(self._tasks.keys()),
"history": self._task_history.copy()
"history": self._task_history.copy(),
}
class StateTracker:
"""Tracks lifecycle state and transitions"""
@@ -228,13 +236,11 @@ class StateTracker:
self._record_state()
def set_state(
self,
state: LifecycleState,
details: Optional[Dict[str, Any]] = None
self, state: LifecycleState, details: Optional[Dict[str, Any]] = None
) -> None:
"""
Set current state.
Args:
state: New state
details: Optional state transition details
@@ -242,10 +248,7 @@ class StateTracker:
self.state = state
self._record_state(details)
def _record_state(
self,
details: Optional[Dict[str, Any]] = None
) -> None:
def _record_state(self, details: Optional[Dict[str, Any]] = None) -> None:
"""Record state transition"""
now = datetime.utcnow()
duration = 0.0
@@ -253,17 +256,20 @@ class StateTracker:
last_state = datetime.fromisoformat(self.state_history[-1]["timestamp"])
duration = (now - last_state).total_seconds()
self.state_history.append(StateHistory(
state=self.state.name,
timestamp=now.isoformat(),
duration=duration,
details=details
))
self.state_history.append(
StateHistory(
state=self.state.name,
timestamp=now.isoformat(),
duration=duration,
details=details,
)
)
def get_state_history(self) -> List[StateHistory]:
"""Get state transition history"""
return self.state_history.copy()
class LifecycleManager:
"""Manages the lifecycle of the VideoArchiver cog"""
@@ -278,12 +284,11 @@ class LifecycleManager:
self._cleanup_handlers: Set[Callable] = set()
def register_cleanup_handler(
self,
handler: Union[Callable[[], None], Callable[[], Any]]
self, handler: Union[Callable[[], None], Callable[[], Any]]
) -> None:
"""
Register a cleanup handler.
Args:
handler: Cleanup handler function
"""
@@ -292,14 +297,14 @@ class LifecycleManager:
async def initialize_cog(self) -> None:
"""
Initialize all components with proper error handling.
Raises:
ComponentError: If initialization fails
"""
try:
# Initialize components in sequence
await self.cog.component_manager.initialize_components()
# Set ready flag
self.cog.ready.set()
logger.info("VideoArchiver initialization completed successfully")
@@ -311,11 +316,8 @@ class LifecycleManager:
raise ComponentError(
error,
context=ErrorContext(
"LifecycleManager",
"initialize_cog",
None,
ErrorSeverity.HIGH
)
"LifecycleManager", "initialize_cog", None, ErrorSeverity.HIGH
),
)
def init_callback(self, task: asyncio.Task) -> None:
@@ -326,67 +328,57 @@ class LifecycleManager:
self.state_tracker.set_state(LifecycleState.READY)
except asyncio.CancelledError:
logger.warning("Initialization was cancelled")
self.state_tracker.set_state(
LifecycleState.ERROR,
{"reason": "cancelled"}
)
self.state_tracker.set_state(LifecycleState.ERROR, {"reason": "cancelled"})
asyncio.create_task(cleanup_resources(self.cog))
except Exception as e:
logger.error(f"Initialization failed: {str(e)}", exc_info=True)
self.state_tracker.set_state(
LifecycleState.ERROR,
{"error": str(e)}
)
self.state_tracker.set_state(LifecycleState.ERROR, {"error": str(e)})
asyncio.create_task(cleanup_resources(self.cog))
async def handle_load(self) -> None:
"""
Handle cog loading without blocking.
Raises:
VideoArchiverError: If load fails
"""
try:
self.state_tracker.set_state(LifecycleState.INITIALIZING)
# Start initialization as background task
await self.task_manager.create_task(
"initialization",
self.initialize_cog(),
self.init_callback,
timeout=self.INIT_TIMEOUT
timeout=self.INIT_TIMEOUT,
)
logger.info("Initialization started in background")
except Exception as e:
self.state_tracker.set_state(LifecycleState.ERROR)
# Ensure cleanup on any error
try:
await asyncio.wait_for(
force_cleanup_resources(self.cog),
timeout=self.CLEANUP_TIMEOUT
force_cleanup_resources(self.cog), timeout=self.CLEANUP_TIMEOUT
)
except asyncio.TimeoutError:
logger.error("Force cleanup during load error timed out")
raise VideoArchiverError(
f"Error during cog load: {str(e)}",
context=ErrorContext(
"LifecycleManager",
"handle_load",
None,
ErrorSeverity.HIGH
)
"LifecycleManager", "handle_load", None, ErrorSeverity.HIGH
),
)
async def handle_unload(self) -> None:
"""
Clean up when cog is unloaded.
Raises:
CleanupError: If cleanup fails
"""
self.state_tracker.set_state(LifecycleState.UNLOADING)
try:
# Cancel all tasks
await self.task_manager.cancel_all_tasks()
@@ -397,13 +389,11 @@ class LifecycleManager:
# Try normal cleanup
try:
cleanup_task = await self.task_manager.create_task(
"cleanup",
cleanup_resources(self.cog),
timeout=self.UNLOAD_TIMEOUT
"cleanup", cleanup_resources(self.cog), timeout=self.UNLOAD_TIMEOUT
)
await cleanup_task
logger.info("Normal cleanup completed")
except (asyncio.TimeoutError, Exception) as e:
if isinstance(e, asyncio.TimeoutError):
logger.warning("Normal cleanup timed out, forcing cleanup")
@@ -413,8 +403,7 @@ class LifecycleManager:
# Force cleanup
try:
await asyncio.wait_for(
force_cleanup_resources(self.cog),
timeout=self.CLEANUP_TIMEOUT
force_cleanup_resources(self.cog), timeout=self.CLEANUP_TIMEOUT
)
logger.info("Force cleanup completed")
except asyncio.TimeoutError:
@@ -426,8 +415,8 @@ class LifecycleManager:
"LifecycleManager",
"handle_unload",
None,
ErrorSeverity.CRITICAL
)
ErrorSeverity.CRITICAL,
),
)
except Exception as e:
error = f"Error during force cleanup: {str(e)}"
@@ -438,25 +427,19 @@ class LifecycleManager:
"LifecycleManager",
"handle_unload",
None,
ErrorSeverity.CRITICAL
)
ErrorSeverity.CRITICAL,
),
)
except Exception as e:
error = f"Error during cog unload: {str(e)}"
logger.error(error, exc_info=True)
self.state_tracker.set_state(
LifecycleState.ERROR,
{"error": str(e)}
)
self.state_tracker.set_state(LifecycleState.ERROR, {"error": str(e)})
raise CleanupError(
error,
context=ErrorContext(
"LifecycleManager",
"handle_unload",
None,
ErrorSeverity.CRITICAL
)
"LifecycleManager", "handle_unload", None, ErrorSeverity.CRITICAL
),
)
finally:
# Clear all references
@@ -487,7 +470,7 @@ class LifecycleManager:
def get_status(self) -> LifecycleStatus:
"""
Get current lifecycle status.
Returns:
Dictionary containing lifecycle status information
"""
@@ -495,5 +478,5 @@ class LifecycleManager:
state=self.state_tracker.state.name,
state_history=self.state_tracker.get_state_history(),
tasks=self.task_manager.get_task_status(),
health=self.state_tracker.state == LifecycleState.READY
health=self.state_tracker.state == LifecycleState.READY,
)

View File

@@ -4,10 +4,10 @@ import logging
from enum import Enum, auto
from typing import Optional, Union, Dict, Any, TypedDict, ClassVar
from datetime import datetime
import discord
from redbot.core.commands import Context
import discord # type: ignore
from redbot.core.commands import Context # type: ignore
from .utils.exceptions import ErrorSeverity
from ..utils.exceptions import ErrorSeverity
logger = logging.getLogger("VideoArchiver")

View File

@@ -4,7 +4,7 @@ from typing import Dict, Any, List, Optional, Union, TypedDict, ClassVar
from dataclasses import dataclass, field
from enum import Enum, auto
from .utils.exceptions import (
from ..utils.exceptions import (
ConfigurationError,
ErrorContext,
ErrorSeverity