- Add Serie.ensure_folder_with_year() method to ensure folder names include year - Update all NFO API endpoints to call ensure_folder_with_year() before operations - Folder format is now 'Name (Year)' when year is available - Add comprehensive tests for ensure_folder_with_year() method - All 5 tests passing
708 lines
22 KiB
Python
708 lines
22 KiB
Python
"""NFO Management API endpoints.
|
|
|
|
This module provides REST API endpoints for managing tvshow.nfo files
|
|
and associated media (poster, logo, fanart).
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
|
|
from src.config.settings import settings
|
|
from src.core.entities.series import Serie
|
|
from src.core.SeriesApp import SeriesApp
|
|
from src.core.services.nfo_service import NFOService
|
|
from src.core.services.tmdb_client import TMDBAPIError
|
|
from src.server.models.nfo import (
|
|
MediaDownloadRequest,
|
|
MediaFilesStatus,
|
|
NFOBatchCreateRequest,
|
|
NFOBatchCreateResponse,
|
|
NFOBatchResult,
|
|
NFOCheckResponse,
|
|
NFOContentResponse,
|
|
NFOCreateRequest,
|
|
NFOCreateResponse,
|
|
NFOMissingResponse,
|
|
NFOMissingSeries,
|
|
)
|
|
from src.server.utils.dependencies import get_series_app, require_auth
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/nfo", tags=["nfo"])
|
|
|
|
|
|
async def get_nfo_service() -> NFOService:
|
|
"""Get NFO service dependency.
|
|
|
|
Returns:
|
|
NFOService instance
|
|
|
|
Raises:
|
|
HTTPException: If NFO service not configured
|
|
"""
|
|
# Check if TMDB API key is in settings
|
|
tmdb_api_key = settings.tmdb_api_key
|
|
|
|
# If not in settings, try to load from config.json
|
|
if not tmdb_api_key:
|
|
try:
|
|
from src.server.services.config_service import get_config_service
|
|
config_service = get_config_service()
|
|
config = config_service.load_config()
|
|
if config.nfo and config.nfo.tmdb_api_key:
|
|
tmdb_api_key = config.nfo.tmdb_api_key
|
|
except Exception:
|
|
pass # Config loading failed, tmdb_api_key remains None
|
|
|
|
if not tmdb_api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="NFO service not configured. TMDB API key required."
|
|
)
|
|
|
|
return NFOService(
|
|
tmdb_api_key=tmdb_api_key,
|
|
anime_directory=settings.anime_directory,
|
|
image_size=settings.nfo_image_size,
|
|
auto_create=settings.nfo_auto_create
|
|
)
|
|
|
|
|
|
def check_media_files(serie_folder: str) -> MediaFilesStatus:
|
|
"""Check status of media files for a series.
|
|
|
|
Args:
|
|
serie_folder: Series folder name
|
|
|
|
Returns:
|
|
MediaFilesStatus with file existence info
|
|
"""
|
|
folder_path = Path(settings.anime_directory) / serie_folder
|
|
|
|
poster_path = folder_path / "poster.jpg"
|
|
logo_path = folder_path / "logo.png"
|
|
fanart_path = folder_path / "fanart.jpg"
|
|
|
|
return MediaFilesStatus(
|
|
has_poster=poster_path.exists(),
|
|
has_logo=logo_path.exists(),
|
|
has_fanart=fanart_path.exists(),
|
|
poster_path=str(poster_path) if poster_path.exists() else None,
|
|
logo_path=str(logo_path) if logo_path.exists() else None,
|
|
fanart_path=str(fanart_path) if fanart_path.exists() else None
|
|
)
|
|
|
|
|
|
@router.get("/{serie_id}/check", response_model=NFOCheckResponse)
|
|
async def check_nfo(
|
|
serie_id: str,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> NFOCheckResponse:
|
|
"""Check if NFO and media files exist for a series.
|
|
|
|
Args:
|
|
serie_id: Series identifier
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
NFOCheckResponse with NFO and media status
|
|
|
|
Raises:
|
|
HTTPException: If series not found
|
|
"""
|
|
try:
|
|
# Get series info
|
|
series_list = series_app.list.GetList()
|
|
serie = next(
|
|
(s for s in series_list if getattr(s, 'key', None) == serie_id),
|
|
None
|
|
)
|
|
|
|
if not serie:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Series not found: {serie_id}"
|
|
)
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
|
|
# Check NFO
|
|
has_nfo = await nfo_service.check_nfo_exists(serie_folder)
|
|
nfo_path = None
|
|
if has_nfo:
|
|
nfo_path = str(
|
|
Path(settings.anime_directory) / serie_folder / "tvshow.nfo"
|
|
)
|
|
|
|
# Check media files
|
|
media_files = check_media_files(serie_folder)
|
|
|
|
return NFOCheckResponse(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
has_nfo=has_nfo,
|
|
nfo_path=nfo_path,
|
|
media_files=media_files
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error checking NFO for {serie_id}: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to check NFO: {str(e)}"
|
|
) from e
|
|
|
|
|
|
@router.post("/{serie_id}/create", response_model=NFOCreateResponse)
|
|
async def create_nfo(
|
|
serie_id: str,
|
|
request: NFOCreateRequest,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> NFOCreateResponse:
|
|
"""Create NFO file and download media for a series.
|
|
|
|
Args:
|
|
serie_id: Series identifier
|
|
request: NFO creation options
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
NFOCreateResponse with creation result
|
|
|
|
Raises:
|
|
HTTPException: If series not found or creation fails
|
|
"""
|
|
try:
|
|
# Get series info
|
|
series_list = series_app.list.GetList()
|
|
serie = next(
|
|
(s for s in series_list if getattr(s, 'key', None) == serie_id),
|
|
None
|
|
)
|
|
|
|
if not serie:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Series not found: {serie_id}"
|
|
)
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
|
|
# If year not provided in request but serie has year, use it
|
|
year = request.year or serie.year
|
|
|
|
# Check if NFO already exists
|
|
if not request.overwrite_existing:
|
|
has_nfo = await nfo_service.check_nfo_exists(serie_folder)
|
|
if has_nfo:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="NFO already exists. Use overwrite_existing=true"
|
|
)
|
|
|
|
# Create NFO
|
|
serie_name = request.serie_name or serie.name or serie_folder
|
|
nfo_path = await nfo_service.create_tvshow_nfo(
|
|
serie_name=serie_name,
|
|
serie_folder=serie_folder,
|
|
year=year,
|
|
download_poster=request.download_poster,
|
|
download_logo=request.download_logo,
|
|
download_fanart=request.download_fanart
|
|
)
|
|
|
|
# Check media files
|
|
media_files = check_media_files(serie_folder)
|
|
|
|
return NFOCreateResponse(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
nfo_path=str(nfo_path),
|
|
media_files=media_files,
|
|
message="NFO and media files created successfully"
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except TMDBAPIError as e:
|
|
logger.warning(f"TMDB API error creating NFO for {serie_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=f"TMDB API error: {str(e)}"
|
|
) from e
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error creating NFO for {serie_id}: {e}",
|
|
exc_info=True
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to create NFO: {str(e)}"
|
|
) from e
|
|
|
|
|
|
@router.put("/{serie_id}/update", response_model=NFOCreateResponse)
|
|
async def update_nfo(
|
|
serie_id: str,
|
|
download_media: bool = True,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> NFOCreateResponse:
|
|
"""Update existing NFO file with fresh TMDB data.
|
|
|
|
Args:
|
|
serie_id: Series identifier
|
|
download_media: Whether to re-download media files
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
NFOCreateResponse with update result
|
|
|
|
Raises:
|
|
HTTPException: If series or NFO not found
|
|
"""
|
|
try:
|
|
# Get series info
|
|
series_list = series_app.list.GetList()
|
|
serie = next(
|
|
(s for s in series_list if getattr(s, 'key', None) == serie_id),
|
|
None
|
|
)
|
|
|
|
if not serie:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Series not found: {serie_id}"
|
|
)
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
|
|
# Check if NFO exists
|
|
has_nfo = await nfo_service.check_nfo_exists(serie_folder)
|
|
if not has_nfo:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="NFO file not found. Use create endpoint instead."
|
|
)
|
|
|
|
# Update NFO
|
|
nfo_path = await nfo_service.update_tvshow_nfo(
|
|
serie_folder=serie_folder,
|
|
download_media=download_media
|
|
)
|
|
|
|
# Check media files
|
|
media_files = check_media_files(serie_folder)
|
|
|
|
return NFOCreateResponse(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
nfo_path=str(nfo_path),
|
|
media_files=media_files,
|
|
message="NFO updated successfully"
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except TMDBAPIError as e:
|
|
logger.warning(f"TMDB API error updating NFO for {serie_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=f"TMDB API error: {str(e)}"
|
|
) from e
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error updating NFO for {serie_id}: {e}",
|
|
exc_info=True
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to update NFO: {str(e)}"
|
|
) from e
|
|
|
|
|
|
@router.get("/{serie_id}/content", response_model=NFOContentResponse)
|
|
async def get_nfo_content(
|
|
serie_id: str,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> NFOContentResponse:
|
|
"""Get NFO file content for a series.
|
|
|
|
Args:
|
|
serie_id: Series identifier
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
NFOContentResponse with NFO content
|
|
|
|
Raises:
|
|
HTTPException: If series or NFO not found
|
|
"""
|
|
try:
|
|
# Get series info
|
|
series_list = series_app.list.GetList()
|
|
serie = next(
|
|
(s for s in series_list if getattr(s, 'key', None) == serie_id),
|
|
None
|
|
)
|
|
|
|
if not serie:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Series not found: {serie_id}"
|
|
)
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
|
|
# Check if NFO exists
|
|
nfo_path = (
|
|
Path(settings.anime_directory) / serie_folder / "tvshow.nfo"
|
|
)
|
|
if not nfo_path.exists():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="NFO file not found"
|
|
)
|
|
|
|
# Read NFO content
|
|
content = nfo_path.read_text(encoding="utf-8")
|
|
file_size = nfo_path.stat().st_size
|
|
last_modified = datetime.fromtimestamp(nfo_path.stat().st_mtime)
|
|
|
|
return NFOContentResponse(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
content=content,
|
|
file_size=file_size,
|
|
last_modified=last_modified
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error reading NFO content for {serie_id}: {e}",
|
|
exc_info=True
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to read NFO content: {str(e)}"
|
|
) from e
|
|
|
|
|
|
@router.get("/{serie_id}/media/status", response_model=MediaFilesStatus)
|
|
async def get_media_status(
|
|
serie_id: str,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app)
|
|
) -> MediaFilesStatus:
|
|
"""Get media files status for a series.
|
|
|
|
Args:
|
|
serie_id: Series identifier
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
|
|
Returns:
|
|
MediaFilesStatus with file existence info
|
|
|
|
Raises:
|
|
HTTPException: If series not found
|
|
"""
|
|
try:
|
|
# Get series info
|
|
series_list = series_app.list.GetList()
|
|
serie = next(
|
|
(s for s in series_list if getattr(s, 'key', None) == serie_id),
|
|
None
|
|
)
|
|
|
|
if not serie:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Series not found: {serie_id}"
|
|
)
|
|
|
|
return check_media_files(serie.folder)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error checking media status for {serie_id}: {e}",
|
|
exc_info=True
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to check media status: {str(e)}"
|
|
) from e
|
|
|
|
|
|
@router.post("/{serie_id}/media/download", response_model=MediaFilesStatus)
|
|
async def download_media(
|
|
serie_id: str,
|
|
request: MediaDownloadRequest,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> MediaFilesStatus:
|
|
"""Download missing media files for a series.
|
|
|
|
Args:
|
|
serie_id: Series identifier
|
|
request: Media download options
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
MediaFilesStatus after download attempt
|
|
|
|
Raises:
|
|
HTTPException: If series or NFO not found
|
|
"""
|
|
try:
|
|
# Get series info
|
|
series_list = series_app.list.GetList()
|
|
serie = next(
|
|
(s for s in series_list if getattr(s, 'key', None) == serie_id),
|
|
None
|
|
)
|
|
|
|
if not serie:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Series not found: {serie_id}"
|
|
)
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
|
|
# Check if NFO exists (needed for TMDB ID)
|
|
has_nfo = await nfo_service.check_nfo_exists(serie_folder)
|
|
if not has_nfo:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="NFO required for media download. Create NFO first."
|
|
)
|
|
|
|
# For now, update NFO which will re-download media
|
|
# In future, could add standalone media download
|
|
if (request.download_poster or request.download_logo
|
|
or request.download_fanart):
|
|
await nfo_service.update_tvshow_nfo(
|
|
serie_folder=serie_folder,
|
|
download_media=True
|
|
)
|
|
|
|
return check_media_files(serie_folder)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error downloading media for {serie_id}: {e}",
|
|
exc_info=True
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to download media: {str(e)}"
|
|
) from e
|
|
|
|
|
|
@router.post("/batch/create", response_model=NFOBatchCreateResponse)
|
|
async def batch_create_nfo(
|
|
request: NFOBatchCreateRequest,
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> NFOBatchCreateResponse:
|
|
"""Batch create NFO files for multiple series.
|
|
|
|
Args:
|
|
request: Batch creation options
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
NFOBatchCreateResponse with results
|
|
"""
|
|
results: List[NFOBatchResult] = []
|
|
successful = 0
|
|
failed = 0
|
|
skipped = 0
|
|
|
|
# Get all series
|
|
series_list = series_app.list.GetList()
|
|
series_map = {
|
|
getattr(s, 'key', None): s
|
|
for s in series_list
|
|
if getattr(s, 'key', None)
|
|
}
|
|
|
|
# Process each series
|
|
semaphore = asyncio.Semaphore(request.max_concurrent)
|
|
|
|
async def process_serie(serie_id: str) -> NFOBatchResult:
|
|
"""Process a single series."""
|
|
async with semaphore:
|
|
try:
|
|
serie = series_map.get(serie_id)
|
|
if not serie:
|
|
return NFOBatchResult(
|
|
serie_id=serie_id,
|
|
serie_folder="",
|
|
success=False,
|
|
message="Series not found"
|
|
)
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
|
|
# Check if NFO exists
|
|
if request.skip_existing:
|
|
has_nfo = await nfo_service.check_nfo_exists(serie_folder)
|
|
if has_nfo:
|
|
return NFOBatchResult(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
success=False,
|
|
message="Skipped - NFO already exists"
|
|
)
|
|
|
|
# Create NFO
|
|
nfo_path = await nfo_service.create_tvshow_nfo(
|
|
serie_name=serie.name or serie_folder,
|
|
serie_folder=serie_folder,
|
|
download_poster=request.download_media,
|
|
download_logo=request.download_media,
|
|
download_fanart=request.download_media
|
|
)
|
|
|
|
return NFOBatchResult(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
success=True,
|
|
message="NFO created successfully",
|
|
nfo_path=str(nfo_path)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error creating NFO for {serie_id}: {e}",
|
|
exc_info=True
|
|
)
|
|
return NFOBatchResult(
|
|
serie_id=serie_id,
|
|
serie_folder=serie.folder if serie else "",
|
|
success=False,
|
|
message=f"Error: {str(e)}"
|
|
)
|
|
|
|
# Process all series concurrently
|
|
tasks = [process_serie(sid) for sid in request.serie_ids]
|
|
results = await asyncio.gather(*tasks)
|
|
|
|
# Count results
|
|
for result in results:
|
|
if result.success:
|
|
successful += 1
|
|
elif "Skipped" in result.message:
|
|
skipped += 1
|
|
else:
|
|
failed += 1
|
|
|
|
return NFOBatchCreateResponse(
|
|
total=len(request.serie_ids),
|
|
successful=successful,
|
|
failed=failed,
|
|
skipped=skipped,
|
|
results=results
|
|
)
|
|
|
|
|
|
@router.get("/missing", response_model=NFOMissingResponse)
|
|
async def get_missing_nfo(
|
|
_auth: dict = Depends(require_auth),
|
|
series_app: SeriesApp = Depends(get_series_app),
|
|
nfo_service: NFOService = Depends(get_nfo_service)
|
|
) -> NFOMissingResponse:
|
|
"""Get list of series without NFO files.
|
|
|
|
Args:
|
|
_auth: Authentication dependency
|
|
series_app: Series app dependency
|
|
nfo_service: NFO service dependency
|
|
|
|
Returns:
|
|
NFOMissingResponse with series list
|
|
"""
|
|
try:
|
|
series_list = series_app.list.GetList()
|
|
missing_series: List[NFOMissingSeries] = []
|
|
|
|
for serie in series_list:
|
|
serie_id = getattr(serie, 'key', None)
|
|
if not serie_id:
|
|
continue
|
|
|
|
# Ensure folder name includes year if available
|
|
serie_folder = serie.ensure_folder_with_year()
|
|
has_nfo = await nfo_service.check_nfo_exists(serie_folder)
|
|
|
|
if not has_nfo:
|
|
media_files = check_media_files(serie_folder)
|
|
has_media = (
|
|
media_files.has_poster
|
|
or media_files.has_logo
|
|
or media_files.has_fanart
|
|
)
|
|
|
|
missing_series.append(NFOMissingSeries(
|
|
serie_id=serie_id,
|
|
serie_folder=serie_folder,
|
|
serie_name=serie.name or serie_folder,
|
|
has_media=has_media,
|
|
media_files=media_files
|
|
))
|
|
|
|
return NFOMissingResponse(
|
|
total_series=len(series_list),
|
|
missing_nfo_count=len(missing_series),
|
|
series=missing_series
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting missing NFOs: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to get missing NFOs: {str(e)}"
|
|
) from e
|