""" Custom Exception Classes Provides specific exception types for better error handling and reporting. Replaces broad 'except Exception' handlers with specific, meaningful exceptions. """ from typing import Optional, Dict, Any from fastapi import HTTPException # ============================================================================ # BASE EXCEPTIONS # ============================================================================ class MediaDownloaderError(Exception): """Base exception for all Media Downloader errors""" def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): self.message = message self.details = details or {} super().__init__(self.message) def to_dict(self) -> Dict[str, Any]: return { "error": self.__class__.__name__, "message": self.message, "details": self.details } # ============================================================================ # DATABASE EXCEPTIONS # ============================================================================ class DatabaseError(MediaDownloaderError): """Database operation failed""" pass class DatabaseConnectionError(DatabaseError): """Failed to connect to database""" pass class DatabaseQueryError(DatabaseError): """Database query failed""" pass class RecordNotFoundError(DatabaseError): """Requested record not found in database""" pass # Alias for generic "not found" scenarios NotFoundError = RecordNotFoundError class DuplicateRecordError(DatabaseError): """Attempted to insert duplicate record""" pass # ============================================================================ # DOWNLOAD EXCEPTIONS # ============================================================================ class DownloadError(MediaDownloaderError): """Download operation failed""" pass class NetworkError(DownloadError): """Network request failed""" pass class RateLimitError(DownloadError): """Rate limit exceeded""" pass class AuthenticationError(DownloadError): """Authentication failed for external service""" pass class PlatformUnavailableError(DownloadError): """Platform or service is unavailable""" pass class ContentNotFoundError(DownloadError): """Requested content not found on platform""" pass class InvalidURLError(DownloadError): """Invalid or malformed URL""" pass # ============================================================================ # FILE OPERATION EXCEPTIONS # ============================================================================ class FileOperationError(MediaDownloaderError): """File operation failed""" pass class MediaFileNotFoundError(FileOperationError): """File not found on filesystem""" pass class FileAccessError(FileOperationError): """Cannot access file (permissions, etc.)""" pass class FileHashError(FileOperationError): """File hash computation failed""" pass class FileMoveError(FileOperationError): """Failed to move file""" pass class ThumbnailError(FileOperationError): """Thumbnail generation failed""" pass # ============================================================================ # VALIDATION EXCEPTIONS # ============================================================================ class ValidationError(MediaDownloaderError): """Input validation failed""" pass class InvalidParameterError(ValidationError): """Invalid parameter value""" pass class MissingParameterError(ValidationError): """Required parameter missing""" pass class PathTraversalError(ValidationError): """Path traversal attempt detected""" pass # ============================================================================ # AUTHENTICATION/AUTHORIZATION EXCEPTIONS # ============================================================================ class AuthError(MediaDownloaderError): """Authentication or authorization error""" pass class TokenExpiredError(AuthError): """Authentication token has expired""" pass class InvalidTokenError(AuthError): """Authentication token is invalid""" pass class InsufficientPermissionsError(AuthError): """User lacks required permissions""" pass # ============================================================================ # SERVICE EXCEPTIONS # ============================================================================ class ServiceError(MediaDownloaderError): """External service error""" pass class FlareSolverrError(ServiceError): """FlareSolverr service error""" pass class RedisError(ServiceError): """Redis cache error""" pass class SchedulerError(ServiceError): """Scheduler service error""" pass # ============================================================================ # FACE RECOGNITION EXCEPTIONS # ============================================================================ class FaceRecognitionError(MediaDownloaderError): """Face recognition operation failed""" pass class NoFaceDetectedError(FaceRecognitionError): """No face detected in image""" pass class FaceEncodingError(FaceRecognitionError): """Failed to encode face""" pass # ============================================================================ # HTTP EXCEPTION HELPERS # ============================================================================ def to_http_exception(error: MediaDownloaderError) -> HTTPException: """Convert a MediaDownloaderError to an HTTPException""" # Map exception types to HTTP status codes (most-specific subclasses first) status_map = { # 400 Bad Request (subclasses before parent) InvalidParameterError: 400, MissingParameterError: 400, InvalidURLError: 400, ValidationError: 400, # 401 Unauthorized (subclasses before parent) TokenExpiredError: 401, InvalidTokenError: 401, AuthenticationError: 401, AuthError: 401, # 403 Forbidden InsufficientPermissionsError: 403, PathTraversalError: 403, FileAccessError: 403, # 404 Not Found RecordNotFoundError: 404, MediaFileNotFoundError: 404, ContentNotFoundError: 404, # 409 Conflict DuplicateRecordError: 409, # 429 Too Many Requests RateLimitError: 429, # 500 Internal Server Error (subclasses before parent) DatabaseConnectionError: 500, DatabaseQueryError: 500, DatabaseError: 500, # 502 Bad Gateway (subclasses before parent) FlareSolverrError: 502, ServiceError: 502, NetworkError: 502, # 503 Service Unavailable PlatformUnavailableError: 503, SchedulerError: 503, } # Find the most specific matching status code status_code = 500 # Default to internal server error for exc_type, code in status_map.items(): if isinstance(error, exc_type): status_code = code break return HTTPException( status_code=status_code, detail=error.to_dict() ) # ============================================================================ # EXCEPTION HANDLER DECORATOR # ============================================================================ from functools import wraps from typing import Callable, TypeVar import asyncio T = TypeVar('T') def handle_exceptions(func: Callable[..., T]) -> Callable[..., T]: """ Decorator to convert MediaDownloaderError exceptions to HTTPException. Use this on route handlers for consistent error responses. """ if asyncio.iscoroutinefunction(func): @wraps(func) async def async_wrapper(*args, **kwargs): try: return await func(*args, **kwargs) except MediaDownloaderError as e: raise to_http_exception(e) except HTTPException: raise # Let HTTPException pass through except Exception as e: # Log unexpected exceptions from modules.universal_logger import get_logger logger = get_logger('API') logger.error(f"Unexpected error in {func.__name__}: {e}", module="Core") raise HTTPException( status_code=500, detail={"error": "InternalError", "message": str(e)} ) return async_wrapper else: @wraps(func) def sync_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except MediaDownloaderError as e: raise to_http_exception(e) except HTTPException: raise except Exception as e: from modules.universal_logger import get_logger logger = get_logger('API') logger.error(f"Unexpected error in {func.__name__}: {e}", module="Core") raise HTTPException( status_code=500, detail={"error": "InternalError", "message": str(e)} ) return sync_wrapper