""" Error handling and formatting utilities for consistent error messages across the application. """ import subprocess from pathlib import Path from typing import Any, Dict, Optional class DownloadError(Exception): """Base exception for download-related errors.""" def __init__( self, message: str, error_type: str = "download_error", details: Optional[str] = None, ): self.message = message self.error_type = error_type self.details = details super().__init__(self.message) class YtDlpError(DownloadError): """Exception for yt-dlp specific errors.""" def __init__( self, message: str, exit_code: Optional[int] = None, stderr: Optional[str] = None, ): self.exit_code = exit_code self.stderr = stderr super().__init__( message, "yt_dlp_error", f"Exit code: {exit_code}, Stderr: {stderr}" ) class FileValidationError(DownloadError): """Exception for file validation errors.""" def __init__(self, message: str, file_path: Optional[Path] = None): self.file_path = file_path super().__init__(message, "file_validation_error", f"File: {file_path}") def format_error_message( error_type: str, artist: str, title: str, video_id: Optional[str] = None, channel_name: Optional[str] = None, details: Optional[str] = None, ) -> str: """ Format a consistent error message for tracking and logging. Args: error_type: Type of error (e.g., "yt-dlp failed", "file verification failed") artist: Artist name title: Song title video_id: YouTube video ID (optional) channel_name: Channel name (optional) details: Additional error details (optional) Returns: Formatted error message """ base_msg = f"{error_type}: {artist} - {title}" if video_id: base_msg += f" (Video ID: {video_id})" if channel_name: base_msg += f" (Channel: {channel_name})" if details: base_msg += f" - {details}" return base_msg def handle_yt_dlp_error( exception: subprocess.CalledProcessError, artist: str, title: str, video_id: Optional[str] = None, channel_name: Optional[str] = None, ) -> YtDlpError: """ Handle yt-dlp subprocess errors and create a standardized exception. Args: exception: The CalledProcessError from subprocess.run artist: Artist name title: Song title video_id: YouTube video ID (optional) channel_name: Channel name (optional) Returns: YtDlpError with formatted message """ error_msg = format_error_message( "yt-dlp failed", artist, title, video_id, channel_name, f"exit code {exception.returncode}: {exception.stderr}", ) return YtDlpError( error_msg, exit_code=exception.returncode, stderr=exception.stderr ) def handle_file_validation_error( message: str, file_path: Path, artist: str, title: str, video_id: Optional[str] = None, channel_name: Optional[str] = None, ) -> FileValidationError: """ Handle file validation errors and create a standardized exception. Args: message: Error message file_path: Path to the file that failed validation artist: Artist name title: Song title video_id: YouTube video ID (optional) channel_name: Channel name (optional) Returns: FileValidationError with formatted message """ error_msg = format_error_message( "file validation failed", artist, title, video_id, channel_name, f"{message} - File: {file_path}", ) return FileValidationError(error_msg, file_path) def log_error(error: DownloadError, logger=None) -> None: """ Log an error with consistent formatting. Args: error: DownloadError instance logger: Optional logger instance """ if logger: logger.error(f"❌ {error.message}") if error.details: logger.error(f" Details: {error.details}") else: print(f"❌ {error.message}") if error.details: print(f" Details: {error.details}") def create_error_context( artist: str, title: str, video_id: Optional[str] = None, channel_name: Optional[str] = None, file_path: Optional[Path] = None, ) -> Dict[str, Any]: """ Create a context dictionary for error reporting. Args: artist: Artist name title: Song title video_id: YouTube video ID (optional) channel_name: Channel name (optional) file_path: File path (optional) Returns: Dictionary with error context """ context = { "artist": artist, "title": title, "timestamp": None, # Could be added if needed } if video_id: context["video_id"] = video_id if channel_name: context["channel_name"] = channel_name if file_path: context["file_path"] = str(file_path) return context