feat(nfo): implement NFO diagnostics and repair
- Add NFO API endpoints: diagnostics, repair, validate, needs-repair - Create /settings/nfo page with full NFO management UI - Add NFO status section to edit modal with repair functionality - Add anime details API for edit modal pre-fill data - Fix auth test fixtures in test_nfo_diagnostics_repair.py Implements NFO diagnostics when editing anime series via right-click menu. Users can now check NFO status, see missing tags, and repair NFOs directly from the edit modal or the dedicated NFO settings page.
This commit is contained in:
@@ -16,7 +16,11 @@ from src.server.exceptions import (
|
||||
ServerError,
|
||||
ValidationError,
|
||||
)
|
||||
from src.server.models.anime import AnimeMetadataUpdate
|
||||
from src.server.models.anime import (
|
||||
AnimeDetailsResponse,
|
||||
AnimeMetadataUpdate,
|
||||
TMDBSearchResult,
|
||||
)
|
||||
from src.server.services.anime_service import AnimeService, AnimeServiceError
|
||||
from src.server.services.background_loader_service import BackgroundLoaderService
|
||||
from src.server.utils.dependencies import (
|
||||
@@ -1192,6 +1196,120 @@ async def get_anime(
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get("/{anime_key}/details", response_model=AnimeDetailsResponse)
|
||||
async def get_anime_details(
|
||||
anime_key: str,
|
||||
_auth: dict = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_database_session),
|
||||
) -> AnimeDetailsResponse:
|
||||
"""Get detailed information about a specific anime series for the edit modal.
|
||||
|
||||
Returns comprehensive series metadata including TMDB/TVDB IDs, NFO status,
|
||||
and other details needed to pre-fill the edit form.
|
||||
|
||||
Args:
|
||||
anime_key: Series key (primary identifier)
|
||||
_auth: Authentication dependency
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
AnimeDetailsResponse: Full series details for edit modal
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Series not found
|
||||
"""
|
||||
# Get series from database (authoritative source for IDs and NFO status)
|
||||
series = await AnimeSeriesService.get_by_key(db, anime_key)
|
||||
if not series:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Series with key '{anime_key}' not found",
|
||||
)
|
||||
|
||||
# Format timestamps
|
||||
nfo_created = None
|
||||
nfo_updated = None
|
||||
if series.nfo_created_at:
|
||||
nfo_created = series.nfo_created_at.isoformat()
|
||||
if series.nfo_updated_at:
|
||||
nfo_updated = series.nfo_updated_at.isoformat()
|
||||
|
||||
return AnimeDetailsResponse(
|
||||
key=series.key,
|
||||
name=series.name,
|
||||
folder=series.folder,
|
||||
year=series.year,
|
||||
status=None, # Status not stored in DB, only in NFO/TMDB
|
||||
plot=None, # Plot not stored in DB, only in NFO/TMDB
|
||||
genres=[],
|
||||
studio=[],
|
||||
premiered=None,
|
||||
rating=None,
|
||||
rating_votes=None,
|
||||
tmdb_id=series.tmdb_id,
|
||||
tvdb_id=series.tvdb_id,
|
||||
has_nfo=series.has_nfo,
|
||||
nfo_created_at=nfo_created,
|
||||
nfo_updated_at=nfo_updated,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{anime_key}/tmdb-search", response_model=List[TMDBSearchResult])
|
||||
async def search_tmdb_for_series(
|
||||
anime_key: str,
|
||||
_auth: dict = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_database_session),
|
||||
) -> List[TMDBSearchResult]:
|
||||
"""Search TMDB for a series by its name to find matching metadata.
|
||||
|
||||
Used by the edit modal's "Fetch from TMDB" feature when no TMDB ID
|
||||
is set. Searches TMDB using the series name and returns matches.
|
||||
|
||||
Args:
|
||||
anime_key: Series key to look up
|
||||
_auth: Authentication dependency
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
List[TMDBSearchResult]: Matching TMDB results
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Series not found
|
||||
HTTPException 400: TMDB not configured
|
||||
"""
|
||||
from src.server.nfo.tmdb_client import TMDBClient
|
||||
|
||||
# Get series from database
|
||||
series = await AnimeSeriesService.get_by_key(db, anime_key)
|
||||
if not series:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Series with key '{anime_key}' not found",
|
||||
)
|
||||
|
||||
# Check if TMDB is configured
|
||||
if not settings.tmdb_api_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="TMDB API key not configured",
|
||||
)
|
||||
|
||||
# Search TMDB by series name
|
||||
tmdb_client = TMDBClient(api_key=settings.tmdb_api_key)
|
||||
results = await tmdb_client.search_tv_series(series.name)
|
||||
|
||||
return [
|
||||
TMDBSearchResult(
|
||||
tmdb_id=r["id"],
|
||||
title=r.get("name", ""),
|
||||
year=int(r.get("first_air_date", "0000")[:4]) if r.get("first_air_date") else None,
|
||||
overview=r.get("overview"),
|
||||
vote_average=r.get("vote_average"),
|
||||
)
|
||||
for r in results
|
||||
]
|
||||
|
||||
|
||||
# Maximum allowed input size for security
|
||||
MAX_INPUT_LENGTH = 100000 # 100KB
|
||||
|
||||
|
||||
Reference in New Issue
Block a user