This commit is contained in:
pacnpal
2024-11-16 22:32:08 +00:00
parent b7d99490cf
commit dac21f2fcd
30 changed files with 5854 additions and 2279 deletions

View File

@@ -1,23 +1,39 @@
"""Module for handling queue status display and formatting"""
import discord
from enum import Enum
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Any, List, Optional
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
import discord
from ..utils.exceptions import DisplayError
logger = logging.getLogger("VideoArchiver")
class DisplayTheme:
"""Defines display themes"""
DEFAULT = {
"title_color": discord.Color.blue(),
"success_color": discord.Color.green(),
"warning_color": discord.Color.gold(),
"error_color": discord.Color.red(),
"info_color": discord.Color.blurple()
}
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:
@@ -26,48 +42,116 @@ class DisplayTemplate:
format_string: str
inline: bool = False
order: int = 0
condition: Optional[str] = None
class DisplaySection(Enum):
"""Available display sections"""
QUEUE_STATS = "queue_stats"
DOWNLOADS = "downloads"
COMPRESSIONS = "compressions"
ERRORS = "errors"
HARDWARE = "hardware"
condition: Optional[DisplayCondition] = None
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']
TIME_THRESHOLDS: ClassVar[List[Tuple[float, str]]] = [
(60, 's'),
(3600, 'm'),
(float('inf'), 'h')
]
@staticmethod
def format_bytes(bytes: int) -> str:
"""Format bytes into human readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes < 1024:
return f"{bytes:.1f}{unit}"
bytes /= 1024
return f"{bytes:.1f}TB"
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
"""
if bytes_value < 0:
raise ValueError("Bytes value cannot be negative")
bytes_num = float(bytes_value)
for unit in StatusFormatter.BYTE_UNITS:
if bytes_num < 1024:
return f"{bytes_num:.1f}{unit}"
bytes_num /= 1024
return f"{bytes_num:.1f}TB"
@staticmethod
def format_time(seconds: float) -> str:
"""Format time duration"""
if seconds < 60:
return f"{seconds:.1f}s"
minutes = seconds / 60
if minutes < 60:
return f"{minutes:.1f}m"
hours = minutes / 60
return f"{hours:.1f}h"
"""
Format time duration.
Args:
seconds: Number of seconds to format
Returns:
Formatted time string
Raises:
ValueError: If seconds is negative
"""
if seconds < 0:
raise ValueError("Time value cannot be negative")
for threshold, unit in StatusFormatter.TIME_THRESHOLDS:
if seconds < threshold:
return f"{seconds:.1f}{unit}"
seconds /= 60
return f"{seconds:.1f}h"
@staticmethod
def format_percentage(value: float) -> str:
"""Format percentage value"""
"""
Format percentage value.
Args:
value: Percentage value to format (0-100)
Returns:
Formatted percentage string
Raises:
ValueError: If value is outside valid range
"""
if not 0 <= value <= 100:
raise ValueError("Percentage must be between 0 and 100")
return f"{value:.1f}%"
@staticmethod
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
"""
if max_length < 4: # Need room for "..."
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"""
def __init__(self):
DEFAULT_THEME: ClassVar[DisplayTheme] = DisplayTheme(
title_color=discord.Color.blue(),
success_color=discord.Color.green(),
warning_color=discord.Color.gold(),
error_color=discord.Color.red(),
info_color=discord.Color.blurple()
)
def __init__(self) -> None:
self.templates: Dict[DisplaySection, DisplayTemplate] = {
DisplaySection.QUEUE_STATS: DisplayTemplate(
name="Queue Statistics",
@@ -96,7 +180,8 @@ class DisplayManager:
"Retries: {retries}\n"
"```"
),
order=2
order=2,
condition=DisplayCondition.HAS_DOWNLOADS
),
DisplaySection.COMPRESSIONS: DisplayTemplate(
name="Active Compressions",
@@ -112,12 +197,13 @@ class DisplayManager:
"Hardware Accel: {hardware_accel}\n"
"```"
),
order=3
order=3,
condition=DisplayCondition.HAS_COMPRESSIONS
),
DisplaySection.ERRORS: DisplayTemplate(
name="Error Statistics",
format_string="```\n{error_stats}```",
condition="has_errors",
condition=DisplayCondition.HAS_ERRORS,
order=4
),
DisplaySection.HARDWARE: DisplayTemplate(
@@ -132,63 +218,99 @@ class DisplayManager:
order=5
)
}
self.theme = DisplayTheme.DEFAULT
self.theme = self.DEFAULT_THEME.copy()
class StatusDisplay:
"""Handles formatting and display of queue status information"""
def __init__(self):
def __init__(self) -> None:
self.display_manager = DisplayManager()
self.formatter = StatusFormatter()
@classmethod
async def create_queue_status_embed(
self,
cls,
queue_status: Dict[str, Any],
active_ops: Dict[str, Any]
) -> discord.Embed:
"""Create an embed displaying queue status and active operations"""
embed = discord.Embed(
title="Queue Status Details",
color=self.display_manager.theme["title_color"],
timestamp=datetime.utcnow()
)
"""
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
"""
try:
display = cls()
embed = discord.Embed(
title="Queue Status Details",
color=display.display_manager.theme["title_color"],
timestamp=datetime.utcnow()
)
# Add sections in order
sections = sorted(
self.display_manager.templates.items(),
key=lambda x: x[1].order
)
# Add sections in order
sections = sorted(
display.display_manager.templates.items(),
key=lambda x: x[1].order
)
for section, template in sections:
# Check condition if exists
if template.condition:
if not self._check_condition(template.condition, queue_status, active_ops):
continue
for section, template in sections:
try:
# Check condition if exists
if template.condition:
if not display._check_condition(
template.condition,
queue_status,
active_ops
):
continue
# Add section based on type
if section == DisplaySection.QUEUE_STATS:
self._add_queue_statistics(embed, queue_status, template)
elif section == DisplaySection.DOWNLOADS:
self._add_active_downloads(embed, active_ops.get('downloads', {}), template)
elif section == DisplaySection.COMPRESSIONS:
self._add_active_compressions(embed, active_ops.get('compressions', {}), template)
elif section == DisplaySection.ERRORS:
self._add_error_statistics(embed, queue_status, template)
elif section == DisplaySection.HARDWARE:
self._add_hardware_statistics(embed, queue_status, template)
# Add section based on type
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)
elif section == DisplaySection.COMPRESSIONS:
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:
display._add_hardware_statistics(embed, queue_status, template)
except Exception as e:
logger.error(f"Error adding section {section.value}: {e}")
# Continue with other sections
return embed
return embed
except Exception as e:
error = f"Error creating status embed: {str(e)}"
logger.error(error, exc_info=True)
raise DisplayError(error)
def _check_condition(
self,
condition: str,
condition: DisplayCondition,
queue_status: Dict[str, Any],
active_ops: Dict[str, Any]
) -> bool:
"""Check if condition for displaying section is met"""
if condition == "has_errors":
return bool(queue_status["metrics"]["errors_by_type"])
return True
try:
if condition == DisplayCondition.HAS_ERRORS:
return bool(queue_status.get("metrics", {}).get("errors_by_type"))
elif condition == DisplayCondition.HAS_DOWNLOADS:
return bool(active_ops.get("downloads"))
elif condition == DisplayCondition.HAS_COMPRESSIONS:
return bool(active_ops.get("compressions"))
return True
except Exception as e:
logger.error(f"Error checking condition {condition}: {e}")
return False
def _add_queue_statistics(
self,
@@ -197,22 +319,31 @@ class StatusDisplay:
template: DisplayTemplate
) -> None:
"""Add queue statistics to the embed"""
embed.add_field(
name=template.name,
value=template.format_string.format(
pending=queue_status['pending'],
processing=queue_status['processing'],
completed=queue_status['completed'],
failed=queue_status['failed'],
success_rate=self.formatter.format_percentage(
queue_status['metrics']['success_rate'] * 100
try:
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),
success_rate=self.formatter.format_percentage(
metrics.get('success_rate', 0) * 100
),
avg_processing_time=self.formatter.format_time(
metrics.get('avg_processing_time', 0)
)
),
avg_processing_time=self.formatter.format_time(
queue_status['metrics']['avg_processing_time']
)
),
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
)
def _add_active_downloads(
self,
@@ -221,28 +352,44 @@ class StatusDisplay:
template: DisplayTemplate
) -> None:
"""Add active downloads information to the embed"""
if downloads:
content = []
for url, progress in downloads.items():
content.append(template.format_string.format(
url=url[:50] + "..." if len(url) > 50 else 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)
))
try:
if downloads:
content = []
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)
))
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")
embed.add_field(
name=template.name,
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
)
except Exception as e:
logger.error(f"Error adding active downloads: {e}")
embed.add_field(
name=template.name,
value="".join(content),
inline=template.inline
)
else:
embed.add_field(
name=template.name,
value="```\nNo active downloads```",
value="```\nError displaying downloads```",
inline=template.inline
)
@@ -253,28 +400,44 @@ class StatusDisplay:
template: DisplayTemplate
) -> None:
"""Add active compressions information to the embed"""
if compressions:
content = []
for file_id, progress in compressions.items():
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)
))
try:
if compressions:
content = []
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)
))
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")
embed.add_field(
name=template.name,
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
)
except Exception as e:
logger.error(f"Error adding active compressions: {e}")
embed.add_field(
name=template.name,
value="".join(content),
inline=template.inline
)
else:
embed.add_field(
name=template.name,
value="```\nNo active compressions```",
value="```\nError displaying compressions```",
inline=template.inline
)
@@ -285,14 +448,26 @@ class StatusDisplay:
template: DisplayTemplate
) -> None:
"""Add error statistics to the embed"""
if queue_status["metrics"]["errors_by_type"]:
error_stats = "\n".join(
f"{error_type}: {count}"
for error_type, count in queue_status["metrics"]["errors_by_type"].items()
)
try:
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]
)
if len(errors_by_type) > template.max_items:
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
)
except Exception as e:
logger.error(f"Error adding error statistics: {e}")
embed.add_field(
name=template.name,
value=template.format_string.format(error_stats=error_stats),
value="```\nError displaying error statistics```",
inline=template.inline
)
@@ -303,14 +478,23 @@ class StatusDisplay:
template: DisplayTemplate
) -> None:
"""Add hardware statistics to the embed"""
embed.add_field(
name=template.name,
value=template.format_string.format(
hw_failures=queue_status['metrics']['hardware_accel_failures'],
comp_failures=queue_status['metrics']['compression_failures'],
memory_usage=self.formatter.format_bytes(
queue_status['metrics']['peak_memory_usage'] * 1024 * 1024 # Convert MB to bytes
)
),
inline=template.inline
)
try:
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),
memory_usage=self.formatter.format_bytes(
metrics.get('peak_memory_usage', 0) * 1024 * 1024 # Convert MB to bytes
)
),
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
)