feat: add anime metadata editing and NFO diagnostics
- Add PUT /anime/{key} endpoint for updating anime key, tmdb_id, tvdb_id
- Add NFO diagnostics and repair endpoints (GET/POST /nfo/diagnostics)
- Add edit modal UI with context menu integration
- Add frontend JS modules for context-menu and edit-modal
- Add comprehensive tests for edit, rename, and NFO repair flows
This commit is contained in:
@@ -17,12 +17,14 @@ from src.server.exceptions import (
|
||||
ServerError,
|
||||
ValidationError,
|
||||
)
|
||||
from src.server.models.anime import AnimeMetadataUpdate
|
||||
from src.server.services.anime_service import AnimeService, AnimeServiceError
|
||||
from src.server.services.background_loader_service import BackgroundLoaderService
|
||||
from src.server.services.folder_rename_service import _scan_for_pre_existing_duplicates
|
||||
from src.server.utils.dependencies import (
|
||||
get_anime_service,
|
||||
get_background_loader_service,
|
||||
get_database_session,
|
||||
get_optional_database_session,
|
||||
get_series_app,
|
||||
require_auth,
|
||||
@@ -1186,3 +1188,75 @@ async def get_anime(
|
||||
# Maximum allowed input size for security
|
||||
MAX_INPUT_LENGTH = 100000 # 100KB
|
||||
|
||||
|
||||
@router.put("/{anime_key}")
|
||||
async def update_anime_metadata(
|
||||
anime_key: str,
|
||||
body: AnimeMetadataUpdate,
|
||||
_auth: dict = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_database_session),
|
||||
) -> dict:
|
||||
"""Update anime metadata (key, tmdb_id, tvdb_id).
|
||||
|
||||
Args:
|
||||
anime_key: Current series key to update
|
||||
body: Fields to update (all optional)
|
||||
_auth: Authentication dependency
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Updated series metadata
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Series not found
|
||||
HTTPException 409: Key conflict (new key already exists)
|
||||
HTTPException 422: Validation error
|
||||
"""
|
||||
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",
|
||||
)
|
||||
|
||||
updates = {}
|
||||
|
||||
if body.key is not None and body.key != anime_key:
|
||||
existing = await AnimeSeriesService.get_by_key(db, body.key)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"A series with key '{body.key}' already exists",
|
||||
)
|
||||
updates["key"] = body.key
|
||||
|
||||
if body.tmdb_id is not None:
|
||||
updates["tmdb_id"] = body.tmdb_id
|
||||
|
||||
if body.tvdb_id is not None:
|
||||
updates["tvdb_id"] = body.tvdb_id
|
||||
|
||||
if not updates:
|
||||
return {
|
||||
"key": series.key,
|
||||
"tmdb_id": series.tmdb_id,
|
||||
"tvdb_id": series.tvdb_id,
|
||||
"message": "No changes",
|
||||
}
|
||||
|
||||
updated = await AnimeSeriesService.update(db, series.id, **updates)
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"Updated metadata for '%s': %s",
|
||||
anime_key,
|
||||
updates,
|
||||
)
|
||||
|
||||
return {
|
||||
"key": updated.key,
|
||||
"tmdb_id": updated.tmdb_id,
|
||||
"tvdb_id": updated.tvdb_id,
|
||||
"message": "Metadata updated successfully",
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ from src.core.entities.series import Serie
|
||||
from src.core.SeriesApp import SeriesApp
|
||||
from src.core.services.nfo_factory import get_nfo_factory
|
||||
from src.core.services.nfo_service import NFOService
|
||||
from src.core.services.nfo_repair_service import (
|
||||
REQUIRED_TAGS,
|
||||
NfoRepairService,
|
||||
find_missing_tags,
|
||||
)
|
||||
from src.core.services.tmdb_client import TMDBAPIError
|
||||
from src.server.models.nfo import (
|
||||
MediaDownloadRequest,
|
||||
@@ -27,8 +32,10 @@ from src.server.models.nfo import (
|
||||
NFOContentResponse,
|
||||
NFOCreateRequest,
|
||||
NFOCreateResponse,
|
||||
NfoDiagnosticsResponse,
|
||||
NFOMissingResponse,
|
||||
NFOMissingSeries,
|
||||
NfoRepairResponse,
|
||||
)
|
||||
from src.server.utils.dependencies import get_series_app, require_auth
|
||||
from src.server.utils.media import check_media_files, get_media_file_paths
|
||||
@@ -808,3 +815,142 @@ async def download_media(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to download media: {str(e)}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/{serie_key}/diagnostics", response_model=NfoDiagnosticsResponse)
|
||||
async def get_nfo_diagnostics(
|
||||
serie_key: str,
|
||||
_auth: dict = Depends(require_auth),
|
||||
series_app: SeriesApp = Depends(get_series_app),
|
||||
) -> NfoDiagnosticsResponse:
|
||||
"""Get NFO diagnostics showing missing required tags.
|
||||
|
||||
Args:
|
||||
serie_key: Series key identifier
|
||||
_auth: Authentication dependency
|
||||
series_app: SeriesApp instance
|
||||
|
||||
Returns:
|
||||
NfoDiagnosticsResponse with has_nfo, missing_tags, required_tags
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Series not found
|
||||
"""
|
||||
serie = None
|
||||
for s in series_app.list.GetList():
|
||||
if getattr(s, "key", None) == serie_key:
|
||||
serie = s
|
||||
break
|
||||
|
||||
if not serie:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Series with key '{serie_key}' not found",
|
||||
)
|
||||
|
||||
serie_folder = serie.ensure_folder_with_year()
|
||||
folder_path = Path(settings.anime_directory) / serie_folder
|
||||
nfo_path = folder_path / "tvshow.nfo"
|
||||
|
||||
required_tag_names = list(REQUIRED_TAGS.values())
|
||||
|
||||
if not nfo_path.exists():
|
||||
return NfoDiagnosticsResponse(
|
||||
has_nfo=False,
|
||||
nfo_path=None,
|
||||
missing_tags=required_tag_names,
|
||||
required_tags=required_tag_names,
|
||||
)
|
||||
|
||||
missing = find_missing_tags(nfo_path)
|
||||
|
||||
return NfoDiagnosticsResponse(
|
||||
has_nfo=True,
|
||||
nfo_path=str(nfo_path),
|
||||
missing_tags=missing,
|
||||
required_tags=required_tag_names,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{serie_key}/repair", response_model=NfoRepairResponse)
|
||||
async def repair_nfo(
|
||||
serie_key: str,
|
||||
_auth: dict = Depends(require_auth),
|
||||
series_app: SeriesApp = Depends(get_series_app),
|
||||
nfo_service: NFOService = Depends(get_nfo_service),
|
||||
) -> NfoRepairResponse:
|
||||
"""Repair or recreate NFO file for a series.
|
||||
|
||||
Detects missing required tags and re-fetches metadata from TMDB.
|
||||
|
||||
Args:
|
||||
serie_key: Series key identifier
|
||||
_auth: Authentication dependency
|
||||
series_app: SeriesApp instance
|
||||
nfo_service: NFO service for TMDB operations
|
||||
|
||||
Returns:
|
||||
NfoRepairResponse with success status and details
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Series not found
|
||||
HTTPException 400: Cannot repair (e.g., no TMDB data available)
|
||||
"""
|
||||
serie = None
|
||||
for s in series_app.list.GetList():
|
||||
if getattr(s, "key", None) == serie_key:
|
||||
serie = s
|
||||
break
|
||||
|
||||
if not serie:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Series with key '{serie_key}' not found",
|
||||
)
|
||||
|
||||
serie_folder = serie.ensure_folder_with_year()
|
||||
folder_path = Path(settings.anime_directory) / serie_folder
|
||||
nfo_path = folder_path / "tvshow.nfo"
|
||||
|
||||
# Get missing tags before repair for reporting
|
||||
missing_before = find_missing_tags(nfo_path) if nfo_path.exists() else list(REQUIRED_TAGS.values())
|
||||
|
||||
try:
|
||||
repair_service = NfoRepairService(nfo_service)
|
||||
|
||||
if nfo_path.exists():
|
||||
repaired = await repair_service.repair_series(folder_path, serie_folder)
|
||||
if not repaired:
|
||||
return NfoRepairResponse(
|
||||
success=True,
|
||||
message="NFO is already complete, no repair needed",
|
||||
repaired_tags=[],
|
||||
)
|
||||
else:
|
||||
# No NFO exists — create new one
|
||||
await nfo_service.create_tvshow_nfo(
|
||||
serie_name=serie.name,
|
||||
serie_folder=serie_folder,
|
||||
download_poster=True,
|
||||
download_logo=True,
|
||||
download_fanart=True,
|
||||
)
|
||||
|
||||
return NfoRepairResponse(
|
||||
success=True,
|
||||
message=f"NFO repaired successfully. Fixed {len(missing_before)} missing tags.",
|
||||
repaired_tags=missing_before,
|
||||
)
|
||||
|
||||
except TMDBAPIError as e:
|
||||
logger.warning("NFO repair failed for '%s': %s", serie_key, e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot repair NFO: {str(e)}. Ensure TMDB ID is set.",
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.error("NFO repair error for '%s': %s", serie_key, e, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to repair NFO: {str(e)}",
|
||||
) from e
|
||||
|
||||
Reference in New Issue
Block a user