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

View File

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

View File

@@ -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,
)

View File

@@ -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:

View File

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

View File

@@ -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]]:

View File

@@ -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,
)

View File

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

View File

@@ -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,
)

View File

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