Files
media-downloader/web/backend/core/exceptions.py
Todd 0d7b2b1aab Initial commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:42:55 -04:00

347 lines
9.1 KiB
Python

"""
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