mirror of
https://github.com/pacnpal/Pac-cogs.git
synced 2025-12-20 10:51:05 -05:00
loads of import fixes
This commit is contained in:
@@ -1,27 +1,27 @@
|
||||
"""Video processing module for VideoArchiver"""
|
||||
|
||||
from typing import Dict, Any, Optional, Union, List, Tuple
|
||||
import discord
|
||||
import discord # type: ignore
|
||||
|
||||
from .processor.core import VideoProcessor
|
||||
from .processor.constants import (
|
||||
from ..processor.core import VideoProcessor
|
||||
from ..processor.constants import (
|
||||
REACTIONS,
|
||||
ReactionType,
|
||||
ReactionEmojis,
|
||||
ProgressEmojis,
|
||||
get_reaction,
|
||||
get_progress_emoji
|
||||
get_progress_emoji,
|
||||
)
|
||||
from .processor.url_extractor import (
|
||||
from ..processor.url_extractor import (
|
||||
URLExtractor,
|
||||
URLMetadata,
|
||||
URLPattern,
|
||||
URLType,
|
||||
URLPatternManager,
|
||||
URLValidator,
|
||||
URLMetadataExtractor
|
||||
URLMetadataExtractor,
|
||||
)
|
||||
from .processor.message_validator import (
|
||||
from ..processor.message_validator import (
|
||||
MessageValidator,
|
||||
ValidationContext,
|
||||
ValidationRule,
|
||||
@@ -30,17 +30,17 @@ from .processor.message_validator import (
|
||||
ValidationCache,
|
||||
ValidationStats,
|
||||
ValidationCacheEntry,
|
||||
ValidationError
|
||||
ValidationError,
|
||||
)
|
||||
from .processor.message_handler import MessageHandler
|
||||
from .processor.queue_handler import QueueHandler
|
||||
from .processor.reactions import (
|
||||
from ..processor.message_handler import MessageHandler
|
||||
from ..processor.queue_handler import QueueHandler
|
||||
from ..processor.reactions import (
|
||||
handle_archived_reaction,
|
||||
update_queue_position_reaction,
|
||||
update_progress_reaction,
|
||||
update_download_progress_reaction
|
||||
update_download_progress_reaction,
|
||||
)
|
||||
from .utils import progress_tracker
|
||||
from ..utils import progress_tracker
|
||||
|
||||
# Export public classes and constants
|
||||
__all__ = [
|
||||
@@ -48,7 +48,6 @@ __all__ = [
|
||||
"VideoProcessor",
|
||||
"MessageHandler",
|
||||
"QueueHandler",
|
||||
|
||||
# URL Extraction
|
||||
"URLExtractor",
|
||||
"URLMetadata",
|
||||
@@ -57,7 +56,6 @@ __all__ = [
|
||||
"URLPatternManager",
|
||||
"URLValidator",
|
||||
"URLMetadataExtractor",
|
||||
|
||||
# Message Validation
|
||||
"MessageValidator",
|
||||
"ValidationContext",
|
||||
@@ -68,13 +66,11 @@ __all__ = [
|
||||
"ValidationStats",
|
||||
"ValidationCacheEntry",
|
||||
"ValidationError",
|
||||
|
||||
# Constants and enums
|
||||
"REACTIONS",
|
||||
"ReactionType",
|
||||
"ReactionEmojis",
|
||||
"ProgressEmojis",
|
||||
|
||||
# Helper functions
|
||||
"get_reaction",
|
||||
"get_progress_emoji",
|
||||
@@ -87,7 +83,6 @@ __all__ = [
|
||||
"get_active_operations",
|
||||
"get_validation_stats",
|
||||
"clear_caches",
|
||||
|
||||
# Reaction handlers
|
||||
"handle_archived_reaction",
|
||||
"update_queue_position_reaction",
|
||||
@@ -104,105 +99,114 @@ __description__ = "Video processing module for archiving Discord videos"
|
||||
url_extractor = URLExtractor()
|
||||
message_validator = MessageValidator()
|
||||
|
||||
|
||||
# URL extraction helper functions
|
||||
async def extract_urls(
|
||||
message: discord.Message,
|
||||
enabled_sites: Optional[List[str]] = None
|
||||
message: discord.Message, enabled_sites: Optional[List[str]] = None
|
||||
) -> List[URLMetadata]:
|
||||
"""
|
||||
Extract video URLs from a Discord message.
|
||||
|
||||
|
||||
Args:
|
||||
message: Discord message to extract URLs from
|
||||
enabled_sites: Optional list of enabled site identifiers
|
||||
|
||||
|
||||
Returns:
|
||||
List of URLMetadata objects for extracted URLs
|
||||
"""
|
||||
return await url_extractor.extract_urls(message, enabled_sites)
|
||||
|
||||
|
||||
async def validate_message(
|
||||
message: discord.Message,
|
||||
settings: Dict[str, Any]
|
||||
message: discord.Message, settings: Dict[str, Any]
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate a Discord message.
|
||||
|
||||
|
||||
Args:
|
||||
message: Discord message to validate
|
||||
settings: Guild settings dictionary
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, reason)
|
||||
|
||||
|
||||
Raises:
|
||||
ValidationError: If validation fails unexpectedly
|
||||
"""
|
||||
return await message_validator.validate_message(message, settings)
|
||||
|
||||
|
||||
# Progress tracking helper functions
|
||||
def update_download_progress(url: str, progress_data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Update download progress for a specific URL.
|
||||
|
||||
|
||||
Args:
|
||||
url: The URL being downloaded
|
||||
progress_data: Dictionary containing progress information
|
||||
"""
|
||||
progress_tracker.update_download_progress(url, progress_data)
|
||||
|
||||
|
||||
def complete_download(url: str) -> None:
|
||||
"""
|
||||
Mark a download as complete.
|
||||
|
||||
|
||||
Args:
|
||||
url: The URL that completed downloading
|
||||
"""
|
||||
progress_tracker.complete_download(url)
|
||||
|
||||
|
||||
def increment_download_retries(url: str) -> None:
|
||||
"""
|
||||
Increment retry count for a download.
|
||||
|
||||
|
||||
Args:
|
||||
url: The URL being retried
|
||||
"""
|
||||
progress_tracker.increment_download_retries(url)
|
||||
|
||||
def get_download_progress(url: Optional[str] = None) -> Union[Dict[str, Any], Dict[str, Dict[str, Any]]]:
|
||||
|
||||
def get_download_progress(
|
||||
url: Optional[str] = None,
|
||||
) -> Union[Dict[str, Any], Dict[str, Dict[str, Any]]]:
|
||||
"""
|
||||
Get download progress for a specific URL or all downloads.
|
||||
|
||||
|
||||
Args:
|
||||
url: Optional URL to get progress for. If None, returns all download progress.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing progress information for one or all downloads
|
||||
"""
|
||||
return progress_tracker.get_download_progress(url)
|
||||
|
||||
|
||||
def get_active_operations() -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Get all active operations.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing information about all active operations
|
||||
"""
|
||||
return progress_tracker.get_active_operations()
|
||||
|
||||
|
||||
def get_validation_stats() -> ValidationStats:
|
||||
"""
|
||||
Get message validation statistics.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing validation statistics and rule information
|
||||
"""
|
||||
return message_validator.get_stats()
|
||||
|
||||
|
||||
def clear_caches(message_id: Optional[int] = None) -> None:
|
||||
"""
|
||||
Clear URL and validation caches.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Optional message ID to clear caches for. If None, clears all caches.
|
||||
"""
|
||||
|
||||
@@ -18,9 +18,9 @@ from typing import (
|
||||
)
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .processor.queue_handler import QueueHandler
|
||||
from .ffmpeg.ffmpeg_manager import FFmpegManager
|
||||
from .utils.exceptions import CleanupError
|
||||
from ..processor.queue_handler import QueueHandler
|
||||
from ..ffmpeg.ffmpeg_manager import FFmpegManager
|
||||
from ..utils.exceptions import CleanupError
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
"""Core VideoProcessor class that manages video processing operations"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Tuple, Dict, Any, List, TypedDict, ClassVar
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from enum import auto, Enum
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Tuple, TypedDict
|
||||
|
||||
from .processor.message_handler import MessageHandler
|
||||
from .processor.queue_handler import QueueHandler
|
||||
from .utils import progress_tracker
|
||||
from .processor.status_display import StatusDisplay
|
||||
from .processor.cleanup_manager import CleanupManager, CleanupStrategy
|
||||
from .processor.constants import REACTIONS
|
||||
from .queue.manager import EnhancedVideoQueueManager
|
||||
from .ffmpeg.ffmpeg_manager import FFmpegManager
|
||||
from .database.video_archive_db import VideoArchiveDB
|
||||
from .config_manager import ConfigManager
|
||||
from .utils.exceptions import ProcessorError
|
||||
import discord # type: ignore
|
||||
from discord.ext import commands # type: ignore
|
||||
|
||||
from ..config_manager import ConfigManager
|
||||
from ..database.video_archive_db import VideoArchiveDB
|
||||
from ..ffmpeg.ffmpeg_manager import FFmpegManager
|
||||
from ..processor.cleanup_manager import CleanupManager, CleanupStrategy
|
||||
from ..processor.constants import REACTIONS
|
||||
|
||||
from ..processor.message_handler import MessageHandler
|
||||
from ..processor.queue_handler import QueueHandler
|
||||
from ..processor.status_display import StatusDisplay
|
||||
from ..queue.manager import EnhancedVideoQueueManager
|
||||
from ..utils import progress_tracker
|
||||
from ..utils.exceptions import ProcessorError
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
class ProcessorState(Enum):
|
||||
"""Possible states of the video processor"""
|
||||
|
||||
INITIALIZING = auto()
|
||||
READY = auto()
|
||||
PROCESSING = auto()
|
||||
@@ -31,15 +35,19 @@ class ProcessorState(Enum):
|
||||
ERROR = auto()
|
||||
SHUTDOWN = auto()
|
||||
|
||||
|
||||
class OperationType(Enum):
|
||||
"""Types of processor operations"""
|
||||
|
||||
MESSAGE_PROCESSING = auto()
|
||||
VIDEO_PROCESSING = auto()
|
||||
QUEUE_MANAGEMENT = auto()
|
||||
CLEANUP = auto()
|
||||
|
||||
|
||||
class OperationDetails(TypedDict):
|
||||
"""Type definition for operation details"""
|
||||
|
||||
type: str
|
||||
start_time: datetime
|
||||
end_time: Optional[datetime]
|
||||
@@ -47,16 +55,20 @@ class OperationDetails(TypedDict):
|
||||
details: Dict[str, Any]
|
||||
error: Optional[str]
|
||||
|
||||
|
||||
class OperationStats(TypedDict):
|
||||
"""Type definition for operation statistics"""
|
||||
|
||||
total_operations: int
|
||||
active_operations: int
|
||||
success_count: int
|
||||
error_count: int
|
||||
success_rate: float
|
||||
|
||||
|
||||
class ProcessorStatus(TypedDict):
|
||||
"""Type definition for processor status"""
|
||||
|
||||
state: str
|
||||
health: bool
|
||||
operations: OperationStats
|
||||
@@ -64,6 +76,7 @@ class ProcessorStatus(TypedDict):
|
||||
last_health_check: Optional[str]
|
||||
health_status: Dict[str, bool]
|
||||
|
||||
|
||||
class OperationTracker:
|
||||
"""Tracks processor operations"""
|
||||
|
||||
@@ -75,18 +88,14 @@ class OperationTracker:
|
||||
self.error_count = 0
|
||||
self.success_count = 0
|
||||
|
||||
def start_operation(
|
||||
self,
|
||||
op_type: OperationType,
|
||||
details: Dict[str, Any]
|
||||
) -> str:
|
||||
def start_operation(self, op_type: OperationType, details: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Start tracking an operation.
|
||||
|
||||
|
||||
Args:
|
||||
op_type: Type of operation
|
||||
details: Operation details
|
||||
|
||||
|
||||
Returns:
|
||||
Operation ID string
|
||||
"""
|
||||
@@ -97,30 +106,29 @@ class OperationTracker:
|
||||
end_time=None,
|
||||
status="running",
|
||||
details=details,
|
||||
error=None
|
||||
error=None,
|
||||
)
|
||||
return op_id
|
||||
|
||||
def end_operation(
|
||||
self,
|
||||
op_id: str,
|
||||
success: bool,
|
||||
error: Optional[str] = None
|
||||
self, op_id: str, success: bool, error: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
End tracking an operation.
|
||||
|
||||
|
||||
Args:
|
||||
op_id: Operation ID
|
||||
success: Whether operation succeeded
|
||||
error: Optional error message
|
||||
"""
|
||||
if op_id in self.operations:
|
||||
self.operations[op_id].update({
|
||||
"end_time": datetime.utcnow(),
|
||||
"status": "success" if success else "error",
|
||||
"error": error
|
||||
})
|
||||
self.operations[op_id].update(
|
||||
{
|
||||
"end_time": datetime.utcnow(),
|
||||
"status": "success" if success else "error",
|
||||
"error": error,
|
||||
}
|
||||
)
|
||||
# Move to history
|
||||
self.operation_history.append(self.operations.pop(op_id))
|
||||
# Update counts
|
||||
@@ -131,12 +139,12 @@ class OperationTracker:
|
||||
|
||||
# Cleanup old history if needed
|
||||
if len(self.operation_history) > self.MAX_HISTORY:
|
||||
self.operation_history = self.operation_history[-self.MAX_HISTORY:]
|
||||
self.operation_history = self.operation_history[-self.MAX_HISTORY :]
|
||||
|
||||
def get_active_operations(self) -> Dict[str, OperationDetails]:
|
||||
"""
|
||||
Get currently active operations.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of active operations
|
||||
"""
|
||||
@@ -145,7 +153,7 @@ class OperationTracker:
|
||||
def get_operation_stats(self) -> OperationStats:
|
||||
"""
|
||||
Get operation statistics.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing operation statistics
|
||||
"""
|
||||
@@ -155,9 +163,10 @@ class OperationTracker:
|
||||
active_operations=len(self.operations),
|
||||
success_count=self.success_count,
|
||||
error_count=self.error_count,
|
||||
success_rate=self.success_count / total if total > 0 else 0.0
|
||||
success_rate=self.success_count / total if total > 0 else 0.0,
|
||||
)
|
||||
|
||||
|
||||
class HealthMonitor:
|
||||
"""Monitors processor health"""
|
||||
|
||||
@@ -165,7 +174,7 @@ class HealthMonitor:
|
||||
ERROR_CHECK_INTERVAL: ClassVar[int] = 30 # Seconds between checks after error
|
||||
SUCCESS_RATE_THRESHOLD: ClassVar[float] = 0.9 # 90% success rate threshold
|
||||
|
||||
def __init__(self, processor: 'VideoProcessor') -> None:
|
||||
def __init__(self, processor: "VideoProcessor") -> None:
|
||||
self.processor = processor
|
||||
self.last_check: Optional[datetime] = None
|
||||
self.health_status: Dict[str, bool] = {}
|
||||
@@ -191,13 +200,15 @@ class HealthMonitor:
|
||||
while True:
|
||||
try:
|
||||
self.last_check = datetime.utcnow()
|
||||
|
||||
|
||||
# Check component health
|
||||
self.health_status.update({
|
||||
"queue_handler": self.processor.queue_handler.is_healthy(),
|
||||
"message_handler": self.processor.message_handler.is_healthy(),
|
||||
"progress_tracker": progress_tracker.is_healthy()
|
||||
})
|
||||
self.health_status.update(
|
||||
{
|
||||
"queue_handler": self.processor.queue_handler.is_healthy(),
|
||||
"message_handler": self.processor.message_handler.is_healthy(),
|
||||
"progress_tracker": progress_tracker.is_healthy(),
|
||||
}
|
||||
)
|
||||
|
||||
# Check operation health
|
||||
op_stats = self.processor.operation_tracker.get_operation_stats()
|
||||
@@ -214,12 +225,13 @@ class HealthMonitor:
|
||||
def is_healthy(self) -> bool:
|
||||
"""
|
||||
Check if processor is healthy.
|
||||
|
||||
|
||||
Returns:
|
||||
True if all components are healthy, False otherwise
|
||||
"""
|
||||
return all(self.health_status.values())
|
||||
|
||||
|
||||
class VideoProcessor:
|
||||
"""Handles video processing operations"""
|
||||
|
||||
@@ -230,7 +242,7 @@ class VideoProcessor:
|
||||
components: Dict[int, Dict[str, Any]],
|
||||
queue_manager: Optional[EnhancedVideoQueueManager] = None,
|
||||
ffmpeg_mgr: Optional[FFmpegManager] = None,
|
||||
db: Optional[VideoArchiveDB] = None
|
||||
db: Optional[VideoArchiveDB] = None,
|
||||
) -> None:
|
||||
self.bot = bot
|
||||
self.config = config_manager
|
||||
@@ -249,9 +261,7 @@ class VideoProcessor:
|
||||
self.queue_handler = QueueHandler(bot, config_manager, components)
|
||||
self.message_handler = MessageHandler(bot, config_manager, queue_manager)
|
||||
self.cleanup_manager = CleanupManager(
|
||||
self.queue_handler,
|
||||
ffmpeg_mgr,
|
||||
CleanupStrategy.NORMAL
|
||||
self.queue_handler, ffmpeg_mgr, CleanupStrategy.NORMAL
|
||||
)
|
||||
|
||||
# Pass db to queue handler if it exists
|
||||
@@ -260,7 +270,7 @@ class VideoProcessor:
|
||||
|
||||
# Store queue task reference
|
||||
self._queue_task: Optional[asyncio.Task] = None
|
||||
|
||||
|
||||
# Mark as ready
|
||||
self.state = ProcessorState.READY
|
||||
logger.info("VideoProcessor initialized successfully")
|
||||
@@ -273,7 +283,7 @@ class VideoProcessor:
|
||||
async def start(self) -> None:
|
||||
"""
|
||||
Start processor operations.
|
||||
|
||||
|
||||
Raises:
|
||||
ProcessorError: If startup fails
|
||||
"""
|
||||
@@ -288,21 +298,20 @@ class VideoProcessor:
|
||||
async def process_video(self, item: Any) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Process a video from the queue.
|
||||
|
||||
|
||||
Args:
|
||||
item: Queue item to process
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (success, error_message)
|
||||
|
||||
|
||||
Raises:
|
||||
ProcessorError: If processing fails
|
||||
"""
|
||||
op_id = self.operation_tracker.start_operation(
|
||||
OperationType.VIDEO_PROCESSING,
|
||||
{"item": str(item)}
|
||||
OperationType.VIDEO_PROCESSING, {"item": str(item)}
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
self.state = ProcessorState.PROCESSING
|
||||
result = await self.queue_handler.process_video(item)
|
||||
@@ -321,18 +330,17 @@ class VideoProcessor:
|
||||
async def process_message(self, message: discord.Message) -> None:
|
||||
"""
|
||||
Process a message for video content.
|
||||
|
||||
|
||||
Args:
|
||||
message: Discord message to process
|
||||
|
||||
|
||||
Raises:
|
||||
ProcessorError: If processing fails
|
||||
"""
|
||||
op_id = self.operation_tracker.start_operation(
|
||||
OperationType.MESSAGE_PROCESSING,
|
||||
{"message_id": message.id}
|
||||
OperationType.MESSAGE_PROCESSING, {"message_id": message.id}
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
await self.message_handler.process_message(message)
|
||||
self.operation_tracker.end_operation(op_id, True)
|
||||
@@ -345,15 +353,14 @@ class VideoProcessor:
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up resources and stop processing.
|
||||
|
||||
|
||||
Raises:
|
||||
ProcessorError: If cleanup fails
|
||||
"""
|
||||
op_id = self.operation_tracker.start_operation(
|
||||
OperationType.CLEANUP,
|
||||
{"type": "normal"}
|
||||
OperationType.CLEANUP, {"type": "normal"}
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
self.state = ProcessorState.SHUTDOWN
|
||||
await self.health_monitor.stop_monitoring()
|
||||
@@ -368,15 +375,14 @@ class VideoProcessor:
|
||||
async def force_cleanup(self) -> None:
|
||||
"""
|
||||
Force cleanup of resources.
|
||||
|
||||
|
||||
Raises:
|
||||
ProcessorError: If force cleanup fails
|
||||
"""
|
||||
op_id = self.operation_tracker.start_operation(
|
||||
OperationType.CLEANUP,
|
||||
{"type": "force"}
|
||||
OperationType.CLEANUP, {"type": "force"}
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
self.state = ProcessorState.SHUTDOWN
|
||||
await self.health_monitor.stop_monitoring()
|
||||
@@ -391,7 +397,7 @@ class VideoProcessor:
|
||||
async def show_queue_details(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
Display detailed queue status.
|
||||
|
||||
|
||||
Args:
|
||||
ctx: Command context
|
||||
"""
|
||||
@@ -402,14 +408,13 @@ class VideoProcessor:
|
||||
|
||||
# Get queue status
|
||||
queue_status = self.queue_manager.get_queue_status(ctx.guild.id)
|
||||
|
||||
|
||||
# Get active operations
|
||||
active_ops = self.operation_tracker.get_active_operations()
|
||||
|
||||
# Create and send status embed
|
||||
embed = await StatusDisplay.create_queue_status_embed(
|
||||
queue_status,
|
||||
active_ops
|
||||
queue_status, active_ops
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@@ -421,7 +426,7 @@ class VideoProcessor:
|
||||
def set_queue_task(self, task: asyncio.Task) -> None:
|
||||
"""
|
||||
Set the queue processing task.
|
||||
|
||||
|
||||
Args:
|
||||
task: Queue processing task
|
||||
"""
|
||||
@@ -431,7 +436,7 @@ class VideoProcessor:
|
||||
def get_status(self) -> ProcessorStatus:
|
||||
"""
|
||||
Get processor status.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing processor status information
|
||||
"""
|
||||
@@ -445,5 +450,5 @@ class VideoProcessor:
|
||||
if self.health_monitor.last_check
|
||||
else None
|
||||
),
|
||||
health_status=self.health_monitor.health_status
|
||||
health_status=self.health_monitor.health_status,
|
||||
)
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
"""Message processing and URL extraction for VideoProcessor"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, Any, List, Tuple, Set, TypedDict, ClassVar
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from enum import auto, Enum
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, TypedDict
|
||||
|
||||
from .processor.url_extractor import URLExtractor, URLMetadata
|
||||
from .processor.message_validator import MessageValidator, ValidationError
|
||||
from .processor.queue_processor import QueueProcessor, QueuePriority
|
||||
from .processor.constants import REACTIONS
|
||||
from .queue.manager import EnhancedVideoQueueManager
|
||||
from .config_manager import ConfigManager
|
||||
from .utils.exceptions import MessageHandlerError
|
||||
import discord # type: ignore
|
||||
from discord.ext import commands # type: ignore
|
||||
|
||||
from ..config_manager import ConfigManager
|
||||
from ..processor.constants import REACTIONS
|
||||
from ..processor.message_validator import MessageValidator, ValidationError
|
||||
from ..processor.queue_processor import QueuePriority, QueueProcessor
|
||||
|
||||
from ..processor.url_extractor import URLExtractor, URLMetadata
|
||||
from ..queue.manager import EnhancedVideoQueueManager
|
||||
from ..utils.exceptions import MessageHandlerError
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
class MessageState(Enum):
|
||||
"""Possible states of message processing"""
|
||||
|
||||
RECEIVED = auto()
|
||||
VALIDATING = auto()
|
||||
EXTRACTING = auto()
|
||||
@@ -28,21 +32,27 @@ class MessageState(Enum):
|
||||
FAILED = auto()
|
||||
IGNORED = auto()
|
||||
|
||||
|
||||
class ProcessingStage(Enum):
|
||||
"""Message processing stages"""
|
||||
|
||||
VALIDATION = auto()
|
||||
EXTRACTION = auto()
|
||||
QUEUEING = auto()
|
||||
COMPLETION = auto()
|
||||
|
||||
|
||||
class MessageCacheEntry(TypedDict):
|
||||
"""Type definition for message cache entry"""
|
||||
|
||||
valid: bool
|
||||
reason: Optional[str]
|
||||
timestamp: str
|
||||
|
||||
|
||||
class MessageStatus(TypedDict):
|
||||
"""Type definition for message status"""
|
||||
|
||||
state: Optional[MessageState]
|
||||
stage: Optional[ProcessingStage]
|
||||
error: Optional[str]
|
||||
@@ -50,6 +60,7 @@ class MessageStatus(TypedDict):
|
||||
end_time: Optional[datetime]
|
||||
duration: Optional[float]
|
||||
|
||||
|
||||
class MessageCache:
|
||||
"""Caches message validation results"""
|
||||
|
||||
@@ -61,7 +72,7 @@ class MessageCache:
|
||||
def add(self, message_id: int, result: MessageCacheEntry) -> None:
|
||||
"""
|
||||
Add a result to cache.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
result: Validation result entry
|
||||
@@ -74,10 +85,10 @@ class MessageCache:
|
||||
def get(self, message_id: int) -> Optional[MessageCacheEntry]:
|
||||
"""
|
||||
Get a cached result.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
|
||||
|
||||
Returns:
|
||||
Cached validation entry or None if not found
|
||||
"""
|
||||
@@ -94,6 +105,7 @@ class MessageCache:
|
||||
del self._cache[oldest]
|
||||
del self._access_times[oldest]
|
||||
|
||||
|
||||
class ProcessingTracker:
|
||||
"""Tracks message processing state and progress"""
|
||||
|
||||
@@ -109,7 +121,7 @@ class ProcessingTracker:
|
||||
def start_processing(self, message_id: int) -> None:
|
||||
"""
|
||||
Start tracking a message.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
"""
|
||||
@@ -121,11 +133,11 @@ class ProcessingTracker:
|
||||
message_id: int,
|
||||
state: MessageState,
|
||||
stage: Optional[ProcessingStage] = None,
|
||||
error: Optional[str] = None
|
||||
error: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Update message state.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
state: New message state
|
||||
@@ -143,16 +155,16 @@ class ProcessingTracker:
|
||||
def get_status(self, message_id: int) -> MessageStatus:
|
||||
"""
|
||||
Get processing status for a message.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing message status information
|
||||
"""
|
||||
end_time = self.end_times.get(message_id)
|
||||
start_time = self.start_times.get(message_id)
|
||||
|
||||
|
||||
return MessageStatus(
|
||||
state=self.states.get(message_id),
|
||||
stage=self.stages.get(message_id),
|
||||
@@ -163,29 +175,32 @@ class ProcessingTracker:
|
||||
(end_time - start_time).total_seconds()
|
||||
if end_time and start_time
|
||||
else None
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def is_message_stuck(self, message_id: int) -> bool:
|
||||
"""
|
||||
Check if a message is stuck in processing.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
|
||||
|
||||
Returns:
|
||||
True if message is stuck, False otherwise
|
||||
"""
|
||||
if message_id not in self.states or message_id not in self.start_times:
|
||||
return False
|
||||
|
||||
|
||||
state = self.states[message_id]
|
||||
if state in (MessageState.COMPLETED, MessageState.FAILED, MessageState.IGNORED):
|
||||
return False
|
||||
|
||||
processing_time = (datetime.utcnow() - self.start_times[message_id]).total_seconds()
|
||||
|
||||
processing_time = (
|
||||
datetime.utcnow() - self.start_times[message_id]
|
||||
).total_seconds()
|
||||
return processing_time > self.MAX_PROCESSING_TIME
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
"""Handles processing of messages for video content"""
|
||||
|
||||
@@ -193,14 +208,14 @@ class MessageHandler:
|
||||
self,
|
||||
bot: discord.Client,
|
||||
config_manager: ConfigManager,
|
||||
queue_manager: EnhancedVideoQueueManager
|
||||
queue_manager: EnhancedVideoQueueManager,
|
||||
) -> None:
|
||||
self.bot = bot
|
||||
self.config_manager = config_manager
|
||||
self.url_extractor = URLExtractor()
|
||||
self.message_validator = MessageValidator()
|
||||
self.queue_processor = QueueProcessor(queue_manager)
|
||||
|
||||
|
||||
# Initialize tracking and caching
|
||||
self.tracker = ProcessingTracker()
|
||||
self.validation_cache = MessageCache()
|
||||
@@ -209,10 +224,10 @@ class MessageHandler:
|
||||
async def process_message(self, message: discord.Message) -> None:
|
||||
"""
|
||||
Process a message for video content.
|
||||
|
||||
|
||||
Args:
|
||||
message: Discord message to process
|
||||
|
||||
|
||||
Raises:
|
||||
MessageHandlerError: If there's an error during processing
|
||||
"""
|
||||
@@ -224,11 +239,7 @@ class MessageHandler:
|
||||
await self._process_message_internal(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {str(e)}", exc_info=True)
|
||||
self.tracker.update_state(
|
||||
message.id,
|
||||
MessageState.FAILED,
|
||||
error=str(e)
|
||||
)
|
||||
self.tracker.update_state(message.id, MessageState.FAILED, error=str(e))
|
||||
try:
|
||||
await message.add_reaction(REACTIONS["error"])
|
||||
except Exception as react_error:
|
||||
@@ -237,10 +248,10 @@ class MessageHandler:
|
||||
async def _process_message_internal(self, message: discord.Message) -> None:
|
||||
"""
|
||||
Internal message processing logic.
|
||||
|
||||
|
||||
Args:
|
||||
message: Discord message to process
|
||||
|
||||
|
||||
Raises:
|
||||
MessageHandlerError: If there's an error during processing
|
||||
"""
|
||||
@@ -260,43 +271,38 @@ class MessageHandler:
|
||||
else:
|
||||
# Validate message
|
||||
self.tracker.update_state(
|
||||
message.id,
|
||||
MessageState.VALIDATING,
|
||||
ProcessingStage.VALIDATION
|
||||
message.id, MessageState.VALIDATING, ProcessingStage.VALIDATION
|
||||
)
|
||||
try:
|
||||
is_valid, reason = await self.message_validator.validate_message(
|
||||
message,
|
||||
settings
|
||||
message, settings
|
||||
)
|
||||
# Cache result
|
||||
self.validation_cache.add(message.id, MessageCacheEntry(
|
||||
valid=is_valid,
|
||||
reason=reason,
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
))
|
||||
self.validation_cache.add(
|
||||
message.id,
|
||||
MessageCacheEntry(
|
||||
valid=is_valid,
|
||||
reason=reason,
|
||||
timestamp=datetime.utcnow().isoformat(),
|
||||
),
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise MessageHandlerError(f"Validation failed: {str(e)}")
|
||||
|
||||
if not is_valid:
|
||||
logger.debug(f"Message validation failed: {reason}")
|
||||
self.tracker.update_state(
|
||||
message.id,
|
||||
MessageState.IGNORED,
|
||||
error=reason
|
||||
message.id, MessageState.IGNORED, error=reason
|
||||
)
|
||||
return
|
||||
|
||||
# Extract URLs
|
||||
self.tracker.update_state(
|
||||
message.id,
|
||||
MessageState.EXTRACTING,
|
||||
ProcessingStage.EXTRACTION
|
||||
message.id, MessageState.EXTRACTING, ProcessingStage.EXTRACTION
|
||||
)
|
||||
try:
|
||||
urls: List[URLMetadata] = await self.url_extractor.extract_urls(
|
||||
message,
|
||||
enabled_sites=settings.get("enabled_sites")
|
||||
message, enabled_sites=settings.get("enabled_sites")
|
||||
)
|
||||
if not urls:
|
||||
logger.debug("No valid URLs found in message")
|
||||
@@ -307,24 +313,18 @@ class MessageHandler:
|
||||
|
||||
# Process URLs
|
||||
self.tracker.update_state(
|
||||
message.id,
|
||||
MessageState.PROCESSING,
|
||||
ProcessingStage.QUEUEING
|
||||
message.id, MessageState.PROCESSING, ProcessingStage.QUEUEING
|
||||
)
|
||||
try:
|
||||
await self.queue_processor.process_urls(
|
||||
message,
|
||||
urls,
|
||||
priority=QueuePriority.NORMAL
|
||||
message, urls, priority=QueuePriority.NORMAL
|
||||
)
|
||||
except Exception as e:
|
||||
raise MessageHandlerError(f"Queue processing failed: {str(e)}")
|
||||
|
||||
# Mark completion
|
||||
self.tracker.update_state(
|
||||
message.id,
|
||||
MessageState.COMPLETED,
|
||||
ProcessingStage.COMPLETION
|
||||
message.id, MessageState.COMPLETED, ProcessingStage.COMPLETION
|
||||
)
|
||||
|
||||
except MessageHandlerError:
|
||||
@@ -333,35 +333,28 @@ class MessageHandler:
|
||||
raise MessageHandlerError(f"Unexpected error: {str(e)}")
|
||||
|
||||
async def format_archive_message(
|
||||
self,
|
||||
author: Optional[discord.Member],
|
||||
channel: discord.TextChannel,
|
||||
url: str
|
||||
self, author: Optional[discord.Member], channel: discord.TextChannel, url: str
|
||||
) -> str:
|
||||
"""
|
||||
Format message for archive channel.
|
||||
|
||||
|
||||
Args:
|
||||
author: Optional message author
|
||||
channel: Channel the message was posted in
|
||||
url: URL being archived
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted message string
|
||||
"""
|
||||
return await self.queue_processor.format_archive_message(
|
||||
author,
|
||||
channel,
|
||||
url
|
||||
)
|
||||
return await self.queue_processor.format_archive_message(author, channel, url)
|
||||
|
||||
def get_message_status(self, message_id: int) -> MessageStatus:
|
||||
"""
|
||||
Get processing status for a message.
|
||||
|
||||
|
||||
Args:
|
||||
message_id: Discord message ID
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing message status information
|
||||
"""
|
||||
@@ -370,7 +363,7 @@ class MessageHandler:
|
||||
def is_healthy(self) -> bool:
|
||||
"""
|
||||
Check if handler is healthy.
|
||||
|
||||
|
||||
Returns:
|
||||
True if handler is healthy, False otherwise
|
||||
"""
|
||||
@@ -378,7 +371,9 @@ class MessageHandler:
|
||||
# Check for any stuck messages
|
||||
for message_id in self.tracker.states:
|
||||
if self.tracker.is_message_stuck(message_id):
|
||||
logger.warning(f"Message {message_id} appears to be stuck in processing")
|
||||
logger.warning(
|
||||
f"Message {message_id} appears to be stuck in processing"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,9 +5,9 @@ from enum import Enum, auto
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Tuple, List, Any, Callable, Set, TypedDict, ClassVar
|
||||
from datetime import datetime
|
||||
import discord
|
||||
import discord # type: ignore
|
||||
|
||||
from .utils.exceptions import ValidationError
|
||||
from ..utils.exceptions import ValidationError
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
@@ -6,29 +6,33 @@ import os
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, Any, List, Tuple, Set, TypedDict, ClassVar, Callable
|
||||
from datetime import datetime
|
||||
import discord
|
||||
import discord # type: ignore
|
||||
|
||||
from .utils import progress_tracker
|
||||
from .database.video_archive_db import VideoArchiveDB
|
||||
from .utils.download_manager import DownloadManager
|
||||
from .utils.message_manager import MessageManager
|
||||
from .utils.exceptions import QueueHandlerError
|
||||
from .queue.models import QueueItem
|
||||
from .config_manager import ConfigManager
|
||||
from .processor.constants import REACTIONS
|
||||
from ..utils import progress_tracker
|
||||
from ..database.video_archive_db import VideoArchiveDB
|
||||
from ..utils.download_manager import DownloadManager
|
||||
from ..utils.message_manager import MessageManager
|
||||
from ..utils.exceptions import QueueHandlerError
|
||||
from ..queue.models import QueueItem
|
||||
from ..config_manager import ConfigManager
|
||||
from ..processor.constants import REACTIONS
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
class QueueItemStatus(Enum):
|
||||
"""Status of a queue item"""
|
||||
|
||||
PENDING = auto()
|
||||
PROCESSING = auto()
|
||||
COMPLETED = auto()
|
||||
FAILED = auto()
|
||||
CANCELLED = auto()
|
||||
|
||||
|
||||
class QueueStats(TypedDict):
|
||||
"""Type definition for queue statistics"""
|
||||
|
||||
active_downloads: int
|
||||
processing_items: int
|
||||
completed_items: int
|
||||
@@ -37,6 +41,7 @@ class QueueStats(TypedDict):
|
||||
last_processed: Optional[str]
|
||||
is_healthy: bool
|
||||
|
||||
|
||||
class QueueHandler:
|
||||
"""Handles queue processing and video operations"""
|
||||
|
||||
@@ -48,7 +53,7 @@ class QueueHandler:
|
||||
bot: discord.Client,
|
||||
config_manager: ConfigManager,
|
||||
components: Dict[int, Dict[str, Any]],
|
||||
db: Optional[VideoArchiveDB] = None
|
||||
db: Optional[VideoArchiveDB] = None,
|
||||
) -> None:
|
||||
self.bot = bot
|
||||
self.config_manager = config_manager
|
||||
@@ -64,7 +69,7 @@ class QueueHandler:
|
||||
"failed_items": 0,
|
||||
"average_processing_time": 0.0,
|
||||
"last_processed": None,
|
||||
"is_healthy": True
|
||||
"is_healthy": True,
|
||||
}
|
||||
|
||||
async def process_video(self, item: QueueItem) -> Tuple[bool, Optional[str]]:
|
||||
|
||||
@@ -5,106 +5,111 @@ import asyncio
|
||||
from enum import Enum, auto
|
||||
from typing import List, Optional, Dict, Any, Set, Union, TypedDict, ClassVar
|
||||
from datetime import datetime
|
||||
import discord
|
||||
import discord # type: ignore
|
||||
|
||||
from .queue.models import QueueItem
|
||||
from .queue.manager import EnhancedVideoQueueManager
|
||||
from .processor.constants import REACTIONS
|
||||
from .processor.url_extractor import URLMetadata
|
||||
from .utils.exceptions import QueueProcessingError
|
||||
from ..queue.models import QueueItem
|
||||
from ..queue.manager import EnhancedVideoQueueManager
|
||||
from ..processor.constants import REACTIONS
|
||||
from ..processor.url_extractor import URLMetadata
|
||||
from ..utils.exceptions import QueueProcessingError
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
class QueuePriority(Enum):
|
||||
"""Priority levels for queue processing"""
|
||||
|
||||
HIGH = auto()
|
||||
NORMAL = auto()
|
||||
LOW = auto()
|
||||
|
||||
|
||||
class QueueMetrics(TypedDict):
|
||||
"""Type definition for queue metrics"""
|
||||
|
||||
total_items: int
|
||||
processing_time: float
|
||||
success_rate: float
|
||||
error_rate: float
|
||||
average_size: float
|
||||
|
||||
|
||||
class QueueProcessor:
|
||||
"""Handles processing of video queue items"""
|
||||
|
||||
|
||||
_active_items: ClassVar[Set[int]] = set()
|
||||
_processing_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
|
||||
|
||||
|
||||
def __init__(self, queue_manager: EnhancedVideoQueueManager):
|
||||
self.queue_manager = queue_manager
|
||||
self._metrics: Dict[str, Any] = {
|
||||
'processed_count': 0,
|
||||
'error_count': 0,
|
||||
'total_size': 0,
|
||||
'total_time': 0
|
||||
"processed_count": 0,
|
||||
"error_count": 0,
|
||||
"total_size": 0,
|
||||
"total_time": 0,
|
||||
}
|
||||
|
||||
|
||||
async def process_item(self, item: QueueItem) -> bool:
|
||||
"""
|
||||
Process a single queue item
|
||||
|
||||
|
||||
Args:
|
||||
item: Queue item to process
|
||||
|
||||
|
||||
Returns:
|
||||
bool: Success status
|
||||
"""
|
||||
if item.id in self._active_items:
|
||||
logger.warning(f"Item {item.id} is already being processed")
|
||||
return False
|
||||
|
||||
|
||||
try:
|
||||
self._active_items.add(item.id)
|
||||
start_time = datetime.now()
|
||||
|
||||
|
||||
# Process item logic here
|
||||
# Placeholder for actual video processing
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
processing_time = (datetime.now() - start_time).total_seconds()
|
||||
self._update_metrics(processing_time, True, item.size)
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing item {item.id}: {str(e)}")
|
||||
self._update_metrics(0, False, 0)
|
||||
return False
|
||||
|
||||
|
||||
finally:
|
||||
self._active_items.remove(item.id)
|
||||
|
||||
|
||||
def _update_metrics(self, processing_time: float, success: bool, size: int) -> None:
|
||||
"""Update processing metrics"""
|
||||
self._metrics['processed_count'] += 1
|
||||
self._metrics['total_time'] += processing_time
|
||||
|
||||
self._metrics["processed_count"] += 1
|
||||
self._metrics["total_time"] += processing_time
|
||||
|
||||
if not success:
|
||||
self._metrics['error_count'] += 1
|
||||
|
||||
self._metrics["error_count"] += 1
|
||||
|
||||
if size > 0:
|
||||
self._metrics['total_size'] += size
|
||||
|
||||
self._metrics["total_size"] += size
|
||||
|
||||
def get_metrics(self) -> QueueMetrics:
|
||||
"""Get current processing metrics"""
|
||||
total = self._metrics['processed_count']
|
||||
total = self._metrics["processed_count"]
|
||||
if total == 0:
|
||||
return QueueMetrics(
|
||||
total_items=0,
|
||||
processing_time=0,
|
||||
success_rate=0,
|
||||
error_rate=0,
|
||||
average_size=0
|
||||
average_size=0,
|
||||
)
|
||||
|
||||
|
||||
return QueueMetrics(
|
||||
total_items=total,
|
||||
processing_time=self._metrics['total_time'],
|
||||
success_rate=(total - self._metrics['error_count']) / total,
|
||||
error_rate=self._metrics['error_count'] / total,
|
||||
average_size=self._metrics['total_size'] / total
|
||||
processing_time=self._metrics["total_time"],
|
||||
success_rate=(total - self._metrics["error_count"]) / total,
|
||||
error_rate=self._metrics["error_count"] / total,
|
||||
average_size=self._metrics["total_size"] / total,
|
||||
)
|
||||
|
||||
@@ -4,22 +4,26 @@ import logging
|
||||
import asyncio
|
||||
import re
|
||||
from typing import List, Optional
|
||||
import discord
|
||||
import discord # type: ignore
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .processor.constants import REACTIONS, ReactionType, get_reaction, get_progress_emoji
|
||||
from .database.video_archive_db import VideoArchiveDB
|
||||
from ..processor.constants import (
|
||||
REACTIONS,
|
||||
ReactionType,
|
||||
get_reaction,
|
||||
get_progress_emoji,
|
||||
)
|
||||
from ..database.video_archive_db import VideoArchiveDB
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
async def handle_archived_reaction(
|
||||
message: discord.Message,
|
||||
user: discord.User,
|
||||
db: VideoArchiveDB
|
||||
message: discord.Message, user: discord.User, db: VideoArchiveDB
|
||||
) -> None:
|
||||
"""
|
||||
Handle reaction to archived video message.
|
||||
|
||||
|
||||
Args:
|
||||
message: The Discord message that was reacted to
|
||||
user: The user who added the reaction
|
||||
@@ -27,7 +31,9 @@ async def handle_archived_reaction(
|
||||
"""
|
||||
try:
|
||||
# Check if the reaction is from a user (not the bot) and is the archived reaction
|
||||
if user.bot or str(message.reactions[0].emoji) != get_reaction(ReactionType.ARCHIVED):
|
||||
if user.bot or str(message.reactions[0].emoji) != get_reaction(
|
||||
ReactionType.ARCHIVED
|
||||
):
|
||||
return
|
||||
|
||||
# Extract URLs from the message using regex
|
||||
@@ -37,9 +43,9 @@ async def handle_archived_reaction(
|
||||
# Check each URL in the database
|
||||
for url in urls:
|
||||
# Ensure URL has proper scheme
|
||||
if url.startswith('www.'):
|
||||
url = 'http://' + url
|
||||
|
||||
if url.startswith("www."):
|
||||
url = "http://" + url
|
||||
|
||||
# Validate URL
|
||||
try:
|
||||
result = urlparse(url)
|
||||
@@ -59,14 +65,13 @@ async def handle_archived_reaction(
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling archived reaction: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def update_queue_position_reaction(
|
||||
message: discord.Message,
|
||||
position: int,
|
||||
bot_user: discord.ClientUser
|
||||
message: discord.Message, position: int, bot_user: discord.ClientUser
|
||||
) -> None:
|
||||
"""
|
||||
Update queue position reaction.
|
||||
|
||||
|
||||
Args:
|
||||
message: The Discord message to update reactions on
|
||||
position: Queue position (0-based index)
|
||||
@@ -100,14 +105,13 @@ async def update_queue_position_reaction(
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update queue position reaction: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def update_progress_reaction(
|
||||
message: discord.Message,
|
||||
progress: float,
|
||||
bot_user: discord.ClientUser
|
||||
message: discord.Message, progress: float, bot_user: discord.ClientUser
|
||||
) -> None:
|
||||
"""
|
||||
Update progress reaction based on FFmpeg progress.
|
||||
|
||||
|
||||
Args:
|
||||
message: The Discord message to update reactions on
|
||||
progress: Progress value between 0 and 100
|
||||
@@ -142,14 +146,13 @@ async def update_progress_reaction(
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update progress reaction: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def update_download_progress_reaction(
|
||||
message: discord.Message,
|
||||
progress: float,
|
||||
bot_user: discord.ClientUser
|
||||
message: discord.Message, progress: float, bot_user: discord.ClientUser
|
||||
) -> None:
|
||||
"""
|
||||
Update download progress reaction.
|
||||
|
||||
|
||||
Args:
|
||||
message: The Discord message to update reactions on
|
||||
progress: Progress value between 0 and 100
|
||||
|
||||
@@ -4,40 +4,59 @@ import logging
|
||||
from enum import Enum, auto
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional, Callable, TypeVar, Union, TypedDict, ClassVar, Tuple
|
||||
import discord
|
||||
from typing import (
|
||||
Dict,
|
||||
Any,
|
||||
List,
|
||||
Optional,
|
||||
Callable,
|
||||
TypeVar,
|
||||
Union,
|
||||
TypedDict,
|
||||
ClassVar,
|
||||
Tuple,
|
||||
)
|
||||
import discord # type: ignore
|
||||
|
||||
from .utils.exceptions import DisplayError
|
||||
from ..utils.exceptions import DisplayError
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class DisplayTheme(TypedDict):
|
||||
"""Type definition for display theme"""
|
||||
|
||||
title_color: discord.Color
|
||||
success_color: discord.Color
|
||||
warning_color: discord.Color
|
||||
error_color: discord.Color
|
||||
info_color: discord.Color
|
||||
|
||||
|
||||
class DisplaySection(Enum):
|
||||
"""Available display sections"""
|
||||
|
||||
QUEUE_STATS = auto()
|
||||
DOWNLOADS = auto()
|
||||
COMPRESSIONS = auto()
|
||||
ERRORS = auto()
|
||||
HARDWARE = auto()
|
||||
|
||||
|
||||
class DisplayCondition(Enum):
|
||||
"""Display conditions for sections"""
|
||||
|
||||
HAS_ERRORS = "has_errors"
|
||||
HAS_DOWNLOADS = "has_downloads"
|
||||
HAS_COMPRESSIONS = "has_compressions"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DisplayTemplate:
|
||||
"""Template for status display sections"""
|
||||
|
||||
name: str
|
||||
format_string: str
|
||||
inline: bool = False
|
||||
@@ -46,27 +65,28 @@ class DisplayTemplate:
|
||||
formatter: Optional[Callable[[Dict[str, Any]], str]] = None
|
||||
max_items: int = field(default=5) # Maximum items to display in lists
|
||||
|
||||
|
||||
class StatusFormatter:
|
||||
"""Formats status information for display"""
|
||||
|
||||
BYTE_UNITS: ClassVar[List[str]] = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
BYTE_UNITS: ClassVar[List[str]] = ["B", "KB", "MB", "GB", "TB"]
|
||||
TIME_THRESHOLDS: ClassVar[List[Tuple[float, str]]] = [
|
||||
(60, 's'),
|
||||
(3600, 'm'),
|
||||
(float('inf'), 'h')
|
||||
(60, "s"),
|
||||
(3600, "m"),
|
||||
(float("inf"), "h"),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def format_bytes(bytes_value: Union[int, float]) -> str:
|
||||
"""
|
||||
Format bytes into human readable format.
|
||||
|
||||
|
||||
Args:
|
||||
bytes_value: Number of bytes to format
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted string with appropriate unit
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If bytes_value is negative
|
||||
"""
|
||||
@@ -84,13 +104,13 @@ class StatusFormatter:
|
||||
def format_time(seconds: float) -> str:
|
||||
"""
|
||||
Format time duration.
|
||||
|
||||
|
||||
Args:
|
||||
seconds: Number of seconds to format
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted time string
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If seconds is negative
|
||||
"""
|
||||
@@ -107,13 +127,13 @@ class StatusFormatter:
|
||||
def format_percentage(value: float) -> str:
|
||||
"""
|
||||
Format percentage value.
|
||||
|
||||
|
||||
Args:
|
||||
value: Percentage value to format (0-100)
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted percentage string
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If value is outside valid range
|
||||
"""
|
||||
@@ -125,14 +145,14 @@ class StatusFormatter:
|
||||
def truncate_url(url: str, max_length: int = 50) -> str:
|
||||
"""
|
||||
Truncate URL to specified length.
|
||||
|
||||
|
||||
Args:
|
||||
url: URL to truncate
|
||||
max_length: Maximum length for URL
|
||||
|
||||
|
||||
Returns:
|
||||
Truncated URL string
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If max_length is less than 4
|
||||
"""
|
||||
@@ -140,6 +160,7 @@ class StatusFormatter:
|
||||
raise ValueError("max_length must be at least 4")
|
||||
return f"{url[:max_length]}..." if len(url) > max_length else url
|
||||
|
||||
|
||||
class DisplayManager:
|
||||
"""Manages status display configuration"""
|
||||
|
||||
@@ -148,7 +169,7 @@ class DisplayManager:
|
||||
success_color=discord.Color.green(),
|
||||
warning_color=discord.Color.gold(),
|
||||
error_color=discord.Color.red(),
|
||||
info_color=discord.Color.blurple()
|
||||
info_color=discord.Color.blurple(),
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -165,7 +186,7 @@ class DisplayManager:
|
||||
"Avg Processing Time: {avg_processing_time}\n"
|
||||
"```"
|
||||
),
|
||||
order=1
|
||||
order=1,
|
||||
),
|
||||
DisplaySection.DOWNLOADS: DisplayTemplate(
|
||||
name="Active Downloads",
|
||||
@@ -181,7 +202,7 @@ class DisplayManager:
|
||||
"```"
|
||||
),
|
||||
order=2,
|
||||
condition=DisplayCondition.HAS_DOWNLOADS
|
||||
condition=DisplayCondition.HAS_DOWNLOADS,
|
||||
),
|
||||
DisplaySection.COMPRESSIONS: DisplayTemplate(
|
||||
name="Active Compressions",
|
||||
@@ -198,13 +219,13 @@ class DisplayManager:
|
||||
"```"
|
||||
),
|
||||
order=3,
|
||||
condition=DisplayCondition.HAS_COMPRESSIONS
|
||||
condition=DisplayCondition.HAS_COMPRESSIONS,
|
||||
),
|
||||
DisplaySection.ERRORS: DisplayTemplate(
|
||||
name="Error Statistics",
|
||||
format_string="```\n{error_stats}```",
|
||||
condition=DisplayCondition.HAS_ERRORS,
|
||||
order=4
|
||||
order=4,
|
||||
),
|
||||
DisplaySection.HARDWARE: DisplayTemplate(
|
||||
name="Hardware Statistics",
|
||||
@@ -215,11 +236,12 @@ class DisplayManager:
|
||||
"Peak Memory Usage: {memory_usage}\n"
|
||||
"```"
|
||||
),
|
||||
order=5
|
||||
)
|
||||
order=5,
|
||||
),
|
||||
}
|
||||
self.theme = self.DEFAULT_THEME.copy()
|
||||
|
||||
|
||||
class StatusDisplay:
|
||||
"""Handles formatting and display of queue status information"""
|
||||
|
||||
@@ -229,20 +251,18 @@ class StatusDisplay:
|
||||
|
||||
@classmethod
|
||||
async def create_queue_status_embed(
|
||||
cls,
|
||||
queue_status: Dict[str, Any],
|
||||
active_ops: Dict[str, Any]
|
||||
cls, queue_status: Dict[str, Any], active_ops: Dict[str, Any]
|
||||
) -> discord.Embed:
|
||||
"""
|
||||
Create an embed displaying queue status and active operations.
|
||||
|
||||
|
||||
Args:
|
||||
queue_status: Dictionary containing queue status information
|
||||
active_ops: Dictionary containing active operations information
|
||||
|
||||
|
||||
Returns:
|
||||
Discord embed containing formatted status information
|
||||
|
||||
|
||||
Raises:
|
||||
DisplayError: If there's an error creating the embed
|
||||
"""
|
||||
@@ -251,13 +271,12 @@ class StatusDisplay:
|
||||
embed = discord.Embed(
|
||||
title="Queue Status Details",
|
||||
color=display.display_manager.theme["title_color"],
|
||||
timestamp=datetime.utcnow()
|
||||
timestamp=datetime.utcnow(),
|
||||
)
|
||||
|
||||
# Add sections in order
|
||||
sections = sorted(
|
||||
display.display_manager.templates.items(),
|
||||
key=lambda x: x[1].order
|
||||
display.display_manager.templates.items(), key=lambda x: x[1].order
|
||||
)
|
||||
|
||||
for section, template in sections:
|
||||
@@ -265,9 +284,7 @@ class StatusDisplay:
|
||||
# Check condition if exists
|
||||
if template.condition:
|
||||
if not display._check_condition(
|
||||
template.condition,
|
||||
queue_status,
|
||||
active_ops
|
||||
template.condition, queue_status, active_ops
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -275,9 +292,13 @@ class StatusDisplay:
|
||||
if section == DisplaySection.QUEUE_STATS:
|
||||
display._add_queue_statistics(embed, queue_status, template)
|
||||
elif section == DisplaySection.DOWNLOADS:
|
||||
display._add_active_downloads(embed, active_ops.get('downloads', {}), template)
|
||||
display._add_active_downloads(
|
||||
embed, active_ops.get("downloads", {}), template
|
||||
)
|
||||
elif section == DisplaySection.COMPRESSIONS:
|
||||
display._add_active_compressions(embed, active_ops.get('compressions', {}), template)
|
||||
display._add_active_compressions(
|
||||
embed, active_ops.get("compressions", {}), template
|
||||
)
|
||||
elif section == DisplaySection.ERRORS:
|
||||
display._add_error_statistics(embed, queue_status, template)
|
||||
elif section == DisplaySection.HARDWARE:
|
||||
@@ -297,7 +318,7 @@ class StatusDisplay:
|
||||
self,
|
||||
condition: DisplayCondition,
|
||||
queue_status: Dict[str, Any],
|
||||
active_ops: Dict[str, Any]
|
||||
active_ops: Dict[str, Any],
|
||||
) -> bool:
|
||||
"""Check if condition for displaying section is met"""
|
||||
try:
|
||||
@@ -316,185 +337,214 @@ class StatusDisplay:
|
||||
self,
|
||||
embed: discord.Embed,
|
||||
queue_status: Dict[str, Any],
|
||||
template: DisplayTemplate
|
||||
template: DisplayTemplate,
|
||||
) -> None:
|
||||
"""Add queue statistics to the embed"""
|
||||
try:
|
||||
metrics = queue_status.get('metrics', {})
|
||||
metrics = queue_status.get("metrics", {})
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value=template.format_string.format(
|
||||
pending=queue_status.get('pending', 0),
|
||||
processing=queue_status.get('processing', 0),
|
||||
completed=queue_status.get('completed', 0),
|
||||
failed=queue_status.get('failed', 0),
|
||||
pending=queue_status.get("pending", 0),
|
||||
processing=queue_status.get("processing", 0),
|
||||
completed=queue_status.get("completed", 0),
|
||||
failed=queue_status.get("failed", 0),
|
||||
success_rate=self.formatter.format_percentage(
|
||||
metrics.get('success_rate', 0) * 100
|
||||
metrics.get("success_rate", 0) * 100
|
||||
),
|
||||
avg_processing_time=self.formatter.format_time(
|
||||
metrics.get('avg_processing_time', 0)
|
||||
)
|
||||
metrics.get("avg_processing_time", 0)
|
||||
),
|
||||
),
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding queue statistics: {e}")
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nError displaying queue statistics```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
|
||||
def _add_active_downloads(
|
||||
self,
|
||||
embed: discord.Embed,
|
||||
downloads: Dict[str, Any],
|
||||
template: DisplayTemplate
|
||||
self, embed: discord.Embed, downloads: Dict[str, Any], template: DisplayTemplate
|
||||
) -> None:
|
||||
"""Add active downloads information to the embed"""
|
||||
try:
|
||||
if downloads:
|
||||
content = []
|
||||
for url, progress in list(downloads.items())[:template.max_items]:
|
||||
for url, progress in list(downloads.items())[: template.max_items]:
|
||||
try:
|
||||
content.append(template.format_string.format(
|
||||
url=self.formatter.truncate_url(url),
|
||||
percent=self.formatter.format_percentage(progress.get('percent', 0)),
|
||||
speed=progress.get('speed', 'N/A'),
|
||||
eta=progress.get('eta', 'N/A'),
|
||||
size=f"{self.formatter.format_bytes(progress.get('downloaded_bytes', 0))}/"
|
||||
f"{self.formatter.format_bytes(progress.get('total_bytes', 0))}",
|
||||
start_time=progress.get('start_time', 'N/A'),
|
||||
retries=progress.get('retries', 0)
|
||||
))
|
||||
content.append(
|
||||
template.format_string.format(
|
||||
url=self.formatter.truncate_url(url),
|
||||
percent=self.formatter.format_percentage(
|
||||
progress.get("percent", 0)
|
||||
),
|
||||
speed=progress.get("speed", "N/A"),
|
||||
eta=progress.get("eta", "N/A"),
|
||||
size=f"{self.formatter.format_bytes(progress.get('downloaded_bytes', 0))}/"
|
||||
f"{self.formatter.format_bytes(progress.get('total_bytes', 0))}",
|
||||
start_time=progress.get("start_time", "N/A"),
|
||||
retries=progress.get("retries", 0),
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting download {url}: {e}")
|
||||
continue
|
||||
|
||||
if len(downloads) > template.max_items:
|
||||
content.append(f"\n... and {len(downloads) - template.max_items} more")
|
||||
content.append(
|
||||
f"\n... and {len(downloads) - template.max_items} more"
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="".join(content) if content else "```\nNo active downloads```",
|
||||
inline=template.inline
|
||||
value=(
|
||||
"".join(content) if content else "```\nNo active downloads```"
|
||||
),
|
||||
inline=template.inline,
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nNo active downloads```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding active downloads: {e}")
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nError displaying downloads```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
|
||||
def _add_active_compressions(
|
||||
self,
|
||||
embed: discord.Embed,
|
||||
compressions: Dict[str, Any],
|
||||
template: DisplayTemplate
|
||||
template: DisplayTemplate,
|
||||
) -> None:
|
||||
"""Add active compressions information to the embed"""
|
||||
try:
|
||||
if compressions:
|
||||
content = []
|
||||
for file_id, progress in list(compressions.items())[:template.max_items]:
|
||||
for file_id, progress in list(compressions.items())[
|
||||
: template.max_items
|
||||
]:
|
||||
try:
|
||||
content.append(template.format_string.format(
|
||||
filename=progress.get('filename', 'Unknown'),
|
||||
percent=self.formatter.format_percentage(progress.get('percent', 0)),
|
||||
elapsed_time=progress.get('elapsed_time', 'N/A'),
|
||||
input_size=self.formatter.format_bytes(progress.get('input_size', 0)),
|
||||
current_size=self.formatter.format_bytes(progress.get('current_size', 0)),
|
||||
target_size=self.formatter.format_bytes(progress.get('target_size', 0)),
|
||||
codec=progress.get('codec', 'Unknown'),
|
||||
hardware_accel=progress.get('hardware_accel', False)
|
||||
))
|
||||
content.append(
|
||||
template.format_string.format(
|
||||
filename=progress.get("filename", "Unknown"),
|
||||
percent=self.formatter.format_percentage(
|
||||
progress.get("percent", 0)
|
||||
),
|
||||
elapsed_time=progress.get("elapsed_time", "N/A"),
|
||||
input_size=self.formatter.format_bytes(
|
||||
progress.get("input_size", 0)
|
||||
),
|
||||
current_size=self.formatter.format_bytes(
|
||||
progress.get("current_size", 0)
|
||||
),
|
||||
target_size=self.formatter.format_bytes(
|
||||
progress.get("target_size", 0)
|
||||
),
|
||||
codec=progress.get("codec", "Unknown"),
|
||||
hardware_accel=progress.get("hardware_accel", False),
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting compression {file_id}: {e}")
|
||||
continue
|
||||
|
||||
if len(compressions) > template.max_items:
|
||||
content.append(f"\n... and {len(compressions) - template.max_items} more")
|
||||
content.append(
|
||||
f"\n... and {len(compressions) - template.max_items} more"
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="".join(content) if content else "```\nNo active compressions```",
|
||||
inline=template.inline
|
||||
value=(
|
||||
"".join(content)
|
||||
if content
|
||||
else "```\nNo active compressions```"
|
||||
),
|
||||
inline=template.inline,
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nNo active compressions```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding active compressions: {e}")
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nError displaying compressions```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
|
||||
def _add_error_statistics(
|
||||
self,
|
||||
embed: discord.Embed,
|
||||
queue_status: Dict[str, Any],
|
||||
template: DisplayTemplate
|
||||
template: DisplayTemplate,
|
||||
) -> None:
|
||||
"""Add error statistics to the embed"""
|
||||
try:
|
||||
metrics = queue_status.get('metrics', {})
|
||||
errors_by_type = metrics.get('errors_by_type', {})
|
||||
metrics = queue_status.get("metrics", {})
|
||||
errors_by_type = metrics.get("errors_by_type", {})
|
||||
if errors_by_type:
|
||||
error_stats = "\n".join(
|
||||
f"{error_type}: {count}"
|
||||
for error_type, count in list(errors_by_type.items())[:template.max_items]
|
||||
for error_type, count in list(errors_by_type.items())[
|
||||
: template.max_items
|
||||
]
|
||||
)
|
||||
if len(errors_by_type) > template.max_items:
|
||||
error_stats += f"\n... and {len(errors_by_type) - template.max_items} more"
|
||||
error_stats += (
|
||||
f"\n... and {len(errors_by_type) - template.max_items} more"
|
||||
)
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value=template.format_string.format(error_stats=error_stats),
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding error statistics: {e}")
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nError displaying error statistics```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
|
||||
def _add_hardware_statistics(
|
||||
self,
|
||||
embed: discord.Embed,
|
||||
queue_status: Dict[str, Any],
|
||||
template: DisplayTemplate
|
||||
template: DisplayTemplate,
|
||||
) -> None:
|
||||
"""Add hardware statistics to the embed"""
|
||||
try:
|
||||
metrics = queue_status.get('metrics', {})
|
||||
metrics = queue_status.get("metrics", {})
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value=template.format_string.format(
|
||||
hw_failures=metrics.get('hardware_accel_failures', 0),
|
||||
comp_failures=metrics.get('compression_failures', 0),
|
||||
hw_failures=metrics.get("hardware_accel_failures", 0),
|
||||
comp_failures=metrics.get("compression_failures", 0),
|
||||
memory_usage=self.formatter.format_bytes(
|
||||
metrics.get('peak_memory_usage', 0) * 1024 * 1024 # Convert MB to bytes
|
||||
)
|
||||
metrics.get("peak_memory_usage", 0)
|
||||
* 1024
|
||||
* 1024 # Convert MB to bytes
|
||||
),
|
||||
),
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding hardware statistics: {e}")
|
||||
embed.add_field(
|
||||
name=template.name,
|
||||
value="```\nError displaying hardware statistics```",
|
||||
inline=template.inline
|
||||
inline=template.inline,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional, Set, Pattern, ClassVar
|
||||
from datetime import datetime
|
||||
import discord
|
||||
import discord # type: ignore
|
||||
from urllib.parse import urlparse, parse_qs, ParseResult
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
Reference in New Issue
Block a user