360 lines
11 KiB
Python
360 lines
11 KiB
Python
"""
|
|
Configuration management utilities for the karaoke downloader.
|
|
Provides centralized configuration loading, validation, and management.
|
|
"""
|
|
|
|
import json
|
|
import platform
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional, Union
|
|
|
|
# Default configuration values
|
|
DEFAULT_CONFIG = {
|
|
"download_settings": {
|
|
"format": "best[height<=720][ext=mp4]/best[height<=720]/best[ext=mp4]/best",
|
|
"preferred_resolution": "720p",
|
|
"audio_format": "mp3",
|
|
"audio_quality": "0",
|
|
"subtitle_language": "en",
|
|
"subtitle_format": "srt",
|
|
"write_metadata": False,
|
|
"write_thumbnail": False,
|
|
"write_description": False,
|
|
"write_annotations": False,
|
|
"write_comments": False,
|
|
"write_subtitles": False,
|
|
"embed_metadata": False,
|
|
"add_metadata": False,
|
|
"continue_downloads": True,
|
|
"no_overwrites": True,
|
|
"ignore_errors": True,
|
|
"no_warnings": False,
|
|
},
|
|
"folder_structure": {
|
|
"downloads_dir": "downloads",
|
|
"logs_dir": "logs",
|
|
"tracking_file": "data/karaoke_tracking.json",
|
|
},
|
|
"logging": {
|
|
"level": "INFO",
|
|
"format": "%(asctime)s - %(levelname)s - %(message)s",
|
|
"include_console": True,
|
|
"include_file": True,
|
|
},
|
|
"platform_settings": {
|
|
"auto_detect_platform": True,
|
|
"yt_dlp_paths": {
|
|
"windows": "downloader/yt-dlp.exe",
|
|
"macos": "downloader/yt-dlp_macos"
|
|
}
|
|
},
|
|
"yt_dlp_path": "downloader/yt-dlp.exe",
|
|
}
|
|
|
|
# Resolution mapping for CLI arguments
|
|
RESOLUTION_MAP = {
|
|
"480p": "480",
|
|
"720p": "720",
|
|
"1080p": "1080",
|
|
"1440p": "1440",
|
|
"2160p": "2160",
|
|
}
|
|
|
|
|
|
def detect_platform() -> str:
|
|
"""Detect the current platform and return platform name."""
|
|
system = platform.system().lower()
|
|
if system == "windows":
|
|
return "windows"
|
|
elif system == "darwin":
|
|
return "macos"
|
|
else:
|
|
return "windows" # Default to Windows for other platforms
|
|
|
|
|
|
def get_platform_yt_dlp_path(platform_paths: Dict[str, str]) -> str:
|
|
"""Get the appropriate yt-dlp path for the current platform."""
|
|
platform_name = detect_platform()
|
|
return platform_paths.get(platform_name, platform_paths.get("windows", "downloader/yt-dlp.exe"))
|
|
|
|
|
|
@dataclass
|
|
class DownloadSettings:
|
|
"""Configuration for download settings."""
|
|
|
|
format: str = "best[height<=720][ext=mp4]/best[height<=720]/best[ext=mp4]/best"
|
|
outtmpl: str = "%(title)s_720p.%(ext)s"
|
|
merge_output_format: str = "mp4"
|
|
noplaylist: bool = True
|
|
postprocessors: list = None
|
|
preferred_resolution: str = "720p"
|
|
audio_format: str = "mp3"
|
|
audio_quality: str = "0"
|
|
subtitle_language: str = "en"
|
|
subtitle_format: str = "srt"
|
|
write_metadata: bool = False
|
|
write_thumbnail: bool = False
|
|
write_description: bool = False
|
|
writedescription: bool = False
|
|
write_annotations: bool = False
|
|
writeannotations: bool = False
|
|
write_comments: bool = False
|
|
writecomments: bool = False
|
|
write_subtitles: bool = False
|
|
writesubtitles: bool = False
|
|
writeinfojson: bool = False
|
|
writethumbnail: bool = False
|
|
embed_metadata: bool = False
|
|
add_metadata: bool = False
|
|
continue_downloads: bool = True
|
|
continuedl: bool = True
|
|
no_overwrites: bool = True
|
|
nooverwrites: bool = True
|
|
ignore_errors: bool = True
|
|
ignoreerrors: bool = True
|
|
no_warnings: bool = False
|
|
|
|
def __post_init__(self):
|
|
"""Initialize default values for complex fields."""
|
|
if self.postprocessors is None:
|
|
self.postprocessors = [
|
|
{
|
|
"key": "FFmpegExtractAudio",
|
|
"preferredcodec": "mp3",
|
|
"preferredquality": "0",
|
|
}
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class FolderStructure:
|
|
"""Configuration for folder structure."""
|
|
|
|
downloads_dir: str = "downloads"
|
|
logs_dir: str = "logs"
|
|
tracking_file: str = "data/karaoke_tracking.json"
|
|
|
|
|
|
@dataclass
|
|
class LoggingConfig:
|
|
"""Configuration for logging."""
|
|
|
|
level: str = "INFO"
|
|
format: str = "%(asctime)s - %(levelname)s - %(message)s"
|
|
include_console: bool = True
|
|
include_file: bool = True
|
|
|
|
|
|
@dataclass
|
|
class AppConfig:
|
|
"""Main application configuration."""
|
|
|
|
download_settings: DownloadSettings = field(default_factory=DownloadSettings)
|
|
folder_structure: FolderStructure = field(default_factory=FolderStructure)
|
|
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
yt_dlp_path: str = "downloader/yt-dlp.exe"
|
|
_config_file: Optional[Path] = None
|
|
_last_modified: Optional[datetime] = None
|
|
|
|
|
|
class ConfigManager:
|
|
"""
|
|
Manages application configuration with loading, validation, and caching.
|
|
"""
|
|
|
|
def __init__(self, config_file: Union[str, Path] = "data/config.json"):
|
|
"""
|
|
Initialize the configuration manager.
|
|
|
|
Args:
|
|
config_file: Path to the configuration file
|
|
"""
|
|
self.config_file = Path(config_file)
|
|
self._config: Optional[AppConfig] = None
|
|
self._last_modified: Optional[datetime] = None
|
|
|
|
def load_config(self, force_reload: bool = False) -> AppConfig:
|
|
"""
|
|
Load configuration from file with caching.
|
|
|
|
Args:
|
|
force_reload: Force reload even if file hasn't changed
|
|
|
|
Returns:
|
|
AppConfig instance
|
|
"""
|
|
# Check if we need to reload
|
|
if not force_reload and self._config is not None:
|
|
if self.config_file.exists():
|
|
current_mtime = datetime.fromtimestamp(self.config_file.stat().st_mtime)
|
|
if self._last_modified and current_mtime <= self._last_modified:
|
|
return self._config
|
|
|
|
# Load configuration
|
|
config_data = self._load_config_file()
|
|
self._config = self._create_config_from_dict(config_data)
|
|
self._last_modified = datetime.now()
|
|
|
|
return self._config
|
|
|
|
def _load_config_file(self) -> Dict[str, Any]:
|
|
"""
|
|
Load configuration from file with fallback to defaults.
|
|
|
|
Returns:
|
|
Configuration dictionary
|
|
"""
|
|
if self.config_file.exists():
|
|
try:
|
|
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
file_config = json.load(f)
|
|
# Merge with defaults
|
|
return self._merge_configs(DEFAULT_CONFIG, file_config)
|
|
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
print(f"Warning: Could not load config.json: {e}")
|
|
print("Using default configuration.")
|
|
|
|
return DEFAULT_CONFIG.copy()
|
|
|
|
def _merge_configs(
|
|
self, default: Dict[str, Any], user: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Merge user configuration with defaults.
|
|
|
|
Args:
|
|
default: Default configuration
|
|
user: User configuration
|
|
|
|
Returns:
|
|
Merged configuration
|
|
"""
|
|
merged = default.copy()
|
|
|
|
for key, value in user.items():
|
|
if (
|
|
key in merged
|
|
and isinstance(merged[key], dict)
|
|
and isinstance(value, dict)
|
|
):
|
|
merged[key] = self._merge_configs(merged[key], value)
|
|
else:
|
|
merged[key] = value
|
|
|
|
return merged
|
|
|
|
def _create_config_from_dict(self, config_data: Dict[str, Any]) -> AppConfig:
|
|
"""
|
|
Create AppConfig from dictionary.
|
|
|
|
Args:
|
|
config_data: Configuration dictionary
|
|
|
|
Returns:
|
|
AppConfig instance
|
|
"""
|
|
download_settings = DownloadSettings(**config_data.get("download_settings", {}))
|
|
folder_structure = FolderStructure(**config_data.get("folder_structure", {}))
|
|
logging_config = LoggingConfig(**config_data.get("logging", {}))
|
|
|
|
# Handle platform-specific yt-dlp path
|
|
yt_dlp_path = config_data.get("yt_dlp_path", "downloader/yt-dlp.exe")
|
|
|
|
# Check if platform auto-detection is enabled
|
|
platform_settings = config_data.get("platform_settings", {})
|
|
if platform_settings.get("auto_detect_platform", True):
|
|
platform_paths = platform_settings.get("yt_dlp_paths", {})
|
|
if platform_paths:
|
|
yt_dlp_path = get_platform_yt_dlp_path(platform_paths)
|
|
|
|
return AppConfig(
|
|
download_settings=download_settings,
|
|
folder_structure=folder_structure,
|
|
logging=logging_config,
|
|
yt_dlp_path=yt_dlp_path,
|
|
_config_file=self.config_file,
|
|
)
|
|
|
|
def update_resolution(self, resolution: str) -> None:
|
|
"""
|
|
Update the download format based on resolution.
|
|
|
|
Args:
|
|
resolution: Resolution string (e.g., "720p", "1080p")
|
|
"""
|
|
if self._config is None:
|
|
self.load_config()
|
|
|
|
if resolution in RESOLUTION_MAP:
|
|
height = RESOLUTION_MAP[resolution]
|
|
format_str = f"best[height<={height}][ext=mp4]/best[height<={height}]/best[ext=mp4]/best"
|
|
self._config.download_settings.format = format_str
|
|
self._config.download_settings.preferred_resolution = resolution
|
|
print(f"🎬 Using resolution: {resolution}")
|
|
|
|
def get_config(self) -> AppConfig:
|
|
"""
|
|
Get the current configuration.
|
|
|
|
Returns:
|
|
AppConfig instance
|
|
"""
|
|
if self._config is None:
|
|
return self.load_config()
|
|
return self._config
|
|
|
|
def save_config(self) -> None:
|
|
"""
|
|
Save current configuration to file.
|
|
"""
|
|
if self._config is None:
|
|
return
|
|
|
|
config_dict = {
|
|
"download_settings": self._config.download_settings.__dict__,
|
|
"folder_structure": self._config.folder_structure.__dict__,
|
|
"logging": self._config.logging.__dict__,
|
|
"yt_dlp_path": self._config.yt_dlp_path,
|
|
}
|
|
|
|
# Ensure directory exists
|
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
json.dump(config_dict, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"Configuration saved to {self.config_file}")
|
|
|
|
|
|
# Global configuration manager instance
|
|
_config_manager: Optional[ConfigManager] = None
|
|
|
|
|
|
def get_config_manager() -> ConfigManager:
|
|
"""
|
|
Get the global configuration manager instance.
|
|
|
|
Returns:
|
|
ConfigManager instance
|
|
"""
|
|
global _config_manager
|
|
if _config_manager is None:
|
|
_config_manager = ConfigManager()
|
|
return _config_manager
|
|
|
|
|
|
def load_config(force_reload: bool = False) -> AppConfig:
|
|
"""
|
|
Load configuration using the global manager.
|
|
|
|
Args:
|
|
force_reload: Force reload even if file hasn't changed
|
|
|
|
Returns:
|
|
AppConfig instance
|
|
"""
|
|
return get_config_manager().load_config(force_reload)
|