KaraokeVideoDownloader/karaoke_downloader/error_utils.py

185 lines
5.0 KiB
Python

"""
Error handling and formatting utilities for consistent error messages across the application.
"""
from typing import Optional, Dict, Any
from pathlib import Path
import subprocess
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