Files
Pac-cogs/videoarchiver/core/settings.py
2024-11-17 19:47:18 +00:00

378 lines
13 KiB
Python

"""Module for managing VideoArchiver settings"""
from typing import Dict, Any, List, Optional, Union, TypedDict, ClassVar
from dataclasses import dataclass, field
from enum import Enum, auto
from ..utils.exceptions import (
ConfigurationError,
ErrorContext,
ErrorSeverity
)
class VideoFormat(Enum):
"""Supported video formats"""
MP4 = "mp4"
WEBM = "webm"
MKV = "mkv"
class VideoQuality(Enum):
"""Video quality presets"""
LOW = "low" # 480p
MEDIUM = "medium" # 720p
HIGH = "high" # 1080p
ULTRA = "ultra" # 4K
class SettingCategory(Enum):
"""Setting categories"""
GENERAL = auto()
CHANNELS = auto()
PERMISSIONS = auto()
VIDEO = auto()
MESSAGES = auto()
PERFORMANCE = auto()
FEATURES = auto()
class ValidationResult(TypedDict):
"""Type definition for validation result"""
valid: bool
error: Optional[str]
details: Dict[str, Any]
@dataclass
class SettingDefinition:
"""Defines a setting's properties"""
name: str
category: SettingCategory
default_value: Any
description: str
data_type: type
required: bool = True
min_value: Optional[Union[int, float]] = None
max_value: Optional[Union[int, float]] = None
choices: Optional[List[Any]] = None
depends_on: Optional[str] = None
validation_func: Optional[callable] = None
error_message: Optional[str] = None
def __post_init__(self) -> None:
"""Validate setting definition"""
if self.choices and self.default_value not in self.choices:
raise ConfigurationError(
f"Default value {self.default_value} not in choices {self.choices}",
context=ErrorContext(
"Settings",
"definition_validation",
{"setting": self.name},
ErrorSeverity.HIGH
)
)
if self.min_value is not None and self.max_value is not None:
if self.min_value > self.max_value:
raise ConfigurationError(
f"Min value {self.min_value} greater than max value {self.max_value}",
context=ErrorContext(
"Settings",
"definition_validation",
{"setting": self.name},
ErrorSeverity.HIGH
)
)
class Settings:
"""Manages VideoArchiver settings"""
# Setting definitions
SETTINGS: ClassVar[Dict[str, SettingDefinition]] = {
"enabled": SettingDefinition(
name="enabled",
category=SettingCategory.GENERAL,
default_value=False,
description="Whether the archiver is enabled for this guild",
data_type=bool
),
"archive_channel": SettingDefinition(
name="archive_channel",
category=SettingCategory.CHANNELS,
default_value=None,
description="Channel where archived videos are posted",
data_type=int,
required=False,
error_message="Archive channel must be a valid channel ID"
),
"log_channel": SettingDefinition(
name="log_channel",
category=SettingCategory.CHANNELS,
default_value=None,
description="Channel for logging archiver actions",
data_type=int,
required=False,
error_message="Log channel must be a valid channel ID"
),
"enabled_channels": SettingDefinition(
name="enabled_channels",
category=SettingCategory.CHANNELS,
default_value=[],
description="Channels to monitor (empty means all channels)",
data_type=list,
error_message="Enabled channels must be a list of valid channel IDs"
),
"allowed_roles": SettingDefinition(
name="allowed_roles",
category=SettingCategory.PERMISSIONS,
default_value=[],
description="Roles allowed to use archiver (empty means all roles)",
data_type=list,
error_message="Allowed roles must be a list of valid role IDs"
),
"video_format": SettingDefinition(
name="video_format",
category=SettingCategory.VIDEO,
default_value=VideoFormat.MP4.value,
description="Format for archived videos",
data_type=str,
choices=[format.value for format in VideoFormat],
error_message=f"Video format must be one of: {', '.join(f.value for f in VideoFormat)}"
),
"video_quality": SettingDefinition(
name="video_quality",
category=SettingCategory.VIDEO,
default_value=VideoQuality.HIGH.value,
description="Quality preset for archived videos",
data_type=str,
choices=[quality.value for quality in VideoQuality],
error_message=f"Video quality must be one of: {', '.join(q.value for q in VideoQuality)}"
),
"max_file_size": SettingDefinition(
name="max_file_size",
category=SettingCategory.VIDEO,
default_value=8,
description="Maximum file size in MB",
data_type=int,
min_value=1,
max_value=100,
error_message="Max file size must be between 1 and 100 MB"
),
"message_duration": SettingDefinition(
name="message_duration",
category=SettingCategory.MESSAGES,
default_value=30,
description="Duration to show status messages (seconds)",
data_type=int,
min_value=5,
max_value=300,
error_message="Message duration must be between 5 and 300 seconds"
),
"message_template": SettingDefinition(
name="message_template",
category=SettingCategory.MESSAGES,
default_value="{author} archived a video from {channel}",
description="Template for archive messages",
data_type=str,
error_message="Message template must contain {author} and {channel} placeholders"
),
"concurrent_downloads": SettingDefinition(
name="concurrent_downloads",
category=SettingCategory.PERFORMANCE,
default_value=2,
description="Maximum concurrent downloads",
data_type=int,
min_value=1,
max_value=5,
error_message="Concurrent downloads must be between 1 and 5"
),
"enabled_sites": SettingDefinition(
name="enabled_sites",
category=SettingCategory.FEATURES,
default_value=None,
description="Sites to enable archiving for (None means all sites)",
data_type=list,
required=False,
error_message="Enabled sites must be a list of valid site identifiers"
),
"use_database": SettingDefinition(
name="use_database",
category=SettingCategory.FEATURES,
default_value=False,
description="Enable database tracking of archived videos",
data_type=bool
),
}
@classmethod
def get_setting_definition(cls, setting: str) -> Optional[SettingDefinition]:
"""
Get definition for a setting.
Args:
setting: Setting name
Returns:
Setting definition or None if not found
"""
return cls.SETTINGS.get(setting)
@classmethod
def get_settings_by_category(cls, category: SettingCategory) -> Dict[str, SettingDefinition]:
"""
Get all settings in a category.
Args:
category: Setting category
Returns:
Dictionary of settings in the category
"""
return {
name: definition
for name, definition in cls.SETTINGS.items()
if definition.category == category
}
@classmethod
def validate_setting(cls, setting: str, value: Any) -> ValidationResult:
"""
Validate a setting value.
Args:
setting: Setting name
value: Value to validate
Returns:
Validation result dictionary
Raises:
ConfigurationError: If setting definition is not found
"""
definition = cls.get_setting_definition(setting)
if not definition:
raise ConfigurationError(
f"Unknown setting: {setting}",
context=ErrorContext(
"Settings",
"validation",
{"setting": setting},
ErrorSeverity.HIGH
)
)
details = {
"setting": setting,
"value": value,
"type": type(value).__name__,
"expected_type": definition.data_type.__name__
}
# Check type
if not isinstance(value, definition.data_type):
return ValidationResult(
valid=False,
error=f"Invalid type: expected {definition.data_type.__name__}, got {type(value).__name__}",
details=details
)
# Check required
if definition.required and value is None:
return ValidationResult(
valid=False,
error="Required setting cannot be None",
details=details
)
# Check choices
if definition.choices and value not in definition.choices:
return ValidationResult(
valid=False,
error=f"Value must be one of: {', '.join(map(str, definition.choices))}",
details=details
)
# Check numeric bounds
if isinstance(value, (int, float)):
if definition.min_value is not None and value < definition.min_value:
return ValidationResult(
valid=False,
error=f"Value must be at least {definition.min_value}",
details=details
)
if definition.max_value is not None and value > definition.max_value:
return ValidationResult(
valid=False,
error=f"Value must be at most {definition.max_value}",
details=details
)
# Custom validation
if definition.validation_func:
try:
result = definition.validation_func(value)
if not result:
return ValidationResult(
valid=False,
error=definition.error_message or "Validation failed",
details=details
)
except Exception as e:
return ValidationResult(
valid=False,
error=str(e),
details=details
)
return ValidationResult(
valid=True,
error=None,
details=details
)
@property
def default_guild_settings(self) -> Dict[str, Any]:
"""
Default settings for guild configuration.
Returns:
Dictionary of default settings
"""
return {
name: definition.default_value
for name, definition in self.SETTINGS.items()
}
@classmethod
def get_setting_help(cls, setting: str) -> Optional[str]:
"""
Get help text for a setting.
Args:
setting: Setting name
Returns:
Help text or None if setting not found
"""
definition = cls.get_setting_definition(setting)
if not definition:
return None
help_text = [
f"Setting: {definition.name}",
f"Category: {definition.category.name}",
f"Description: {definition.description}",
f"Type: {definition.data_type.__name__}",
f"Required: {definition.required}",
f"Default: {definition.default_value}"
]
if definition.choices:
help_text.append(f"Choices: {', '.join(map(str, definition.choices))}")
if definition.min_value is not None:
help_text.append(f"Minimum: {definition.min_value}")
if definition.max_value is not None:
help_text.append(f"Maximum: {definition.max_value}")
if definition.depends_on:
help_text.append(f"Depends on: {definition.depends_on}")
if definition.error_message:
help_text.append(f"Error: {definition.error_message}")
return "\n".join(help_text)