feat(anime): rename NFO Diagnostics to Anime Settings + add edit endpoints

Replaces the read-only 'NFO Diagnostics' page with a full per-anime
Settings page reached from the right-click context menu on series cards.
Users can now view and edit key, name, folder, tmdb_id, tvdb_id and site
for each anime; changes are persisted to the DB and optionally written
back to the NFO file or used to regenerate it.

Backend
- Rename NfoDiagnosticsResponse -> NfoSettingsResponse,
  NfoSeriesDiagnostics -> NfoSeriesSettings
- Rename get_nfo_diagnostics -> get_nfo_settings,
  repair_nfo -> repair_nfo_settings
- Fix nfo.py bug: repair was calling non-existent
  update_series_nfo_status(); now uses update_nfo_status() and an
  explicit AnimeSeriesService.update(nfo_path=...)
- New endpoints on /api/anime/{key}:
    GET  /settings            -> AnimeSettingsResponse
    PUT  /settings            -> AnimeSettingsResponse
                                 (body: name/folder/tmdb_id/tvdb_id/site,
                                  options: apply_to_nfo, rename_disk)
    POST /regenerate-nfo      -> AnimeSettingsRegenerateNfoResponse
- New Pydantic models: AnimeSettingsResponse,
  AnimeSettingsUpdateRequest, AnimeSettingsRegenerateNfoResponse
- /anime/settings page route; /settings/nfo now 301-redirects to it

Frontend
- New AniWorld.AnimeSettingsManager JS module (single-page form,
  no tabs) with public API init/loadSeries/saveSettings/regenerateNfo/
  validateField/populateForm/showSaveSuccess/showError
- New anime-settings.html template + anime-settings.css
- Right-click menu: data-action 'nfo-diagnostics' replaced by
  'anime-settings' (label 'Anime Settings'), navigates to
  /anime/settings?key=...
- Library 'Open NFO Diagnostics' link renamed to 'Open Anime Settings'

Bug fix
- context-menu click handler was calling hide() BEFORE building the
  navigation URL, which cleared currentSeriesKey to null and produced
  /anime/settings?key=null. Captures the key into a local const first.
  Regression-locked by tests/frontend/unit/context_menu.test.js.

Tests
- 21 new pytest tests in tests/api/test_anime_settings_endpoints.py
  (GET/PUT/regenerate-nfo, auth, validation, nfo-repair bug regression)
- tests/api/test_nfo_endpoints.py trimmed to 6 focused tests
- 31 new Vitest unit tests for AnimeSettingsManager
- 5 new Vitest unit tests for ContextMenu (incl. source-invariant
  regression guard for the hide()-before-key bug)
- 5 new Playwright E2E tests covering right-click, direct nav,
  legacy /settings/nfo redirect, and context-menu labels
- New vitest.config.js (environment: happy-dom)

Docs
- Docs/API.md: new section 'Anime Settings Endpoints'
- Docs/CHANGELOG.md: documents the rename and the context-menu bug fix

Verified
- pytest: 27/27 (21 new + 6 trimmed nfo)
- vitest: 36/36 (31 anime-settings + 5 context-menu)
- playwright e2e: 5/5
This commit is contained in:
2026-06-21 07:52:22 +02:00
parent e050f6fa2d
commit eabce18e41
22 changed files with 3216 additions and 2302 deletions

View File

@@ -1,6 +1,6 @@
"""NFO Management API endpoints.
Provides endpoints for NFO diagnostics, repair, and validation for anime series.
Provides endpoints for NFO settings, repair, and validation for anime series.
"""
import logging
import os
@@ -11,9 +11,9 @@ from pydantic import BaseModel
from src.config.settings import settings
from src.server.models.nfo import (
NfoDiagnosticsResponse,
NfoRepairResponse,
NfoSeriesDiagnostics,
NfoSeriesSettings,
NfoSettingsResponse,
)
from src.server.services.anime_service import AnimeService
from src.server.utils.dependencies import get_anime_service, require_auth
@@ -62,7 +62,7 @@ class NfoNeedsRepairListResponse(BaseModel):
"""Response listing series that need NFO repair."""
total: int
series: List[NfoSeriesDiagnostics]
series: List[NfoSeriesSettings]
def _get_nfo_path(folder: str) -> str:
@@ -123,13 +123,13 @@ async def _get_series_data(
return None
@router.get("/{key}/diagnostics", response_model=NfoDiagnosticsResponse)
async def get_nfo_diagnostics(
@router.get("/{key}/diagnostics", response_model=NfoSettingsResponse)
async def get_nfo_settings(
key: str,
_auth: dict = Depends(require_auth),
anime_service: AnimeService = Depends(get_anime_service),
) -> NfoDiagnosticsResponse:
"""Get NFO diagnostics for a specific series.
) -> NfoSettingsResponse:
"""Get NFO settings inspection for a specific series.
Checks if tvshow.nfo exists in the series folder and validates
that required tags are present.
@@ -140,7 +140,7 @@ async def get_nfo_diagnostics(
anime_service: AnimeService dependency
Returns:
NfoDiagnosticsResponse with has_nfo, nfo_path, missing_tags, required_tags
NfoSettingsResponse with has_nfo, nfo_path, missing_tags, required_tags
Raises:
HTTPException 404: If series not found
@@ -165,7 +165,7 @@ async def get_nfo_diagnostics(
nfo_exists = os.path.isfile(nfo_path)
if not nfo_exists:
return NfoDiagnosticsResponse(
return NfoSettingsResponse(
has_nfo=False,
nfo_path=None,
missing_tags=REQUIRED_TAGS.copy(),
@@ -175,7 +175,7 @@ async def get_nfo_diagnostics(
# Parse and check for missing tags
_, missing = _parse_nfo_file(nfo_path)
return NfoDiagnosticsResponse(
return NfoSettingsResponse(
has_nfo=True,
nfo_path=nfo_path,
missing_tags=missing,
@@ -184,7 +184,7 @@ async def get_nfo_diagnostics(
@router.post("/{key}/repair", response_model=NfoRepairResponse)
async def repair_nfo(
async def repair_nfo_settings(
key: str,
_auth: dict = Depends(require_auth),
anime_service: AnimeService = Depends(get_anime_service),
@@ -332,12 +332,20 @@ async def _create_or_update_nfo(
logger.info("Created/updated tvshow.nfo for %s at %s", key, nfo_path)
# Update series NFO status in DB
await anime_service.update_series_nfo_status(
await anime_service.update_nfo_status(
key=key,
has_nfo=True,
nfo_path=nfo_path,
)
# Also update nfo_path in DB (not part of update_nfo_status signature)
from src.server.database.connection import get_db_session
from src.server.database.service import AnimeSeriesService
async with get_db_session() as db:
series = await AnimeSeriesService.get_by_key(db, key)
if series is not None:
await AnimeSeriesService.update(db, series.id, nfo_path=nfo_path)
# Return list of repaired tags (all tags that were missing before)
return missing_before
@@ -411,7 +419,7 @@ async def get_series_needing_repair(
NfoNeedsRepairListResponse with total count and list of series diagnostics
"""
all_series = await anime_service.list_series_with_filters()
series_needing_repair: List[NfoSeriesDiagnostics] = []
series_needing_repair: List[NfoSeriesSettings] = []
anime_dir = getattr(settings, "anime_directory", None)
if not anime_dir:
@@ -432,7 +440,7 @@ async def get_series_needing_repair(
nfo_exists = os.path.isfile(nfo_path)
if not nfo_exists:
series_needing_repair.append(NfoSeriesDiagnostics(
series_needing_repair.append(NfoSeriesSettings(
key=key,
name=name,
folder=folder,
@@ -444,7 +452,7 @@ async def get_series_needing_repair(
# Parse and check for missing tags
_, missing = _parse_nfo_file(nfo_path)
if missing:
series_needing_repair.append(NfoSeriesDiagnostics(
series_needing_repair.append(NfoSeriesSettings(
key=key,
name=name,
folder=folder,