347 lines
9.1 KiB
Python
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
|