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
398 lines
11 KiB
Python
398 lines
11 KiB
Python
"""NFO API request and response models.
|
|
|
|
This module defines Pydantic models for NFO management API operations.
|
|
"""
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class MediaFilesStatus(BaseModel):
|
|
"""Status of media files (poster, logo, fanart) for a series.
|
|
|
|
Attributes:
|
|
has_poster: Whether poster.jpg exists
|
|
has_logo: Whether logo.png exists
|
|
has_fanart: Whether fanart.jpg exists
|
|
poster_path: Path to poster file if exists
|
|
logo_path: Path to logo file if exists
|
|
fanart_path: Path to fanart file if exists
|
|
"""
|
|
has_poster: bool = Field(
|
|
default=False,
|
|
description="Whether poster.jpg exists"
|
|
)
|
|
has_logo: bool = Field(
|
|
default=False,
|
|
description="Whether logo.png exists"
|
|
)
|
|
has_fanart: bool = Field(
|
|
default=False,
|
|
description="Whether fanart.jpg exists"
|
|
)
|
|
poster_path: Optional[str] = Field(
|
|
default=None,
|
|
description="Path to poster file if exists"
|
|
)
|
|
logo_path: Optional[str] = Field(
|
|
default=None,
|
|
description="Path to logo file if exists"
|
|
)
|
|
fanart_path: Optional[str] = Field(
|
|
default=None,
|
|
description="Path to fanart file if exists"
|
|
)
|
|
|
|
|
|
class NFOCheckResponse(BaseModel):
|
|
"""Response for NFO existence check.
|
|
|
|
Attributes:
|
|
serie_id: Series identifier
|
|
serie_folder: Series folder name
|
|
has_nfo: Whether tvshow.nfo exists
|
|
nfo_path: Path to NFO file if exists
|
|
media_files: Status of media files
|
|
"""
|
|
serie_id: str = Field(
|
|
...,
|
|
description="Series identifier"
|
|
)
|
|
serie_folder: str = Field(
|
|
...,
|
|
description="Series folder name"
|
|
)
|
|
has_nfo: bool = Field(
|
|
...,
|
|
description="Whether tvshow.nfo exists"
|
|
)
|
|
nfo_path: Optional[str] = Field(
|
|
default=None,
|
|
description="Path to NFO file if exists"
|
|
)
|
|
media_files: MediaFilesStatus = Field(
|
|
...,
|
|
description="Status of media files"
|
|
)
|
|
|
|
|
|
class NFOCreateRequest(BaseModel):
|
|
"""Request to create NFO file.
|
|
|
|
Attributes:
|
|
serie_name: Name to search in TMDB
|
|
year: Optional year to narrow search
|
|
download_poster: Whether to download poster.jpg
|
|
download_logo: Whether to download logo.png
|
|
download_fanart: Whether to download fanart.jpg
|
|
overwrite_existing: Whether to overwrite existing NFO
|
|
"""
|
|
serie_name: Optional[str] = Field(
|
|
default=None,
|
|
description="Name to search in TMDB (defaults to folder name)"
|
|
)
|
|
year: Optional[int] = Field(
|
|
default=None,
|
|
description="Optional year to narrow search"
|
|
)
|
|
download_poster: bool = Field(
|
|
default=True,
|
|
description="Whether to download poster.jpg"
|
|
)
|
|
download_logo: bool = Field(
|
|
default=True,
|
|
description="Whether to download logo.png"
|
|
)
|
|
download_fanart: bool = Field(
|
|
default=True,
|
|
description="Whether to download fanart.jpg"
|
|
)
|
|
overwrite_existing: bool = Field(
|
|
default=False,
|
|
description="Whether to overwrite existing NFO"
|
|
)
|
|
|
|
|
|
class NFOCreateResponse(BaseModel):
|
|
"""Response after NFO creation.
|
|
|
|
Attributes:
|
|
serie_id: Series identifier
|
|
serie_folder: Series folder name
|
|
nfo_path: Path to created NFO file
|
|
media_files: Status of downloaded media files
|
|
tmdb_id: TMDB ID of matched series
|
|
message: Success message
|
|
"""
|
|
serie_id: str = Field(
|
|
...,
|
|
description="Series identifier"
|
|
)
|
|
serie_folder: str = Field(
|
|
...,
|
|
description="Series folder name"
|
|
)
|
|
nfo_path: str = Field(
|
|
...,
|
|
description="Path to created NFO file"
|
|
)
|
|
media_files: MediaFilesStatus = Field(
|
|
...,
|
|
description="Status of downloaded media files"
|
|
)
|
|
tmdb_id: Optional[int] = Field(
|
|
default=None,
|
|
description="TMDB ID of matched series"
|
|
)
|
|
message: str = Field(
|
|
...,
|
|
description="Success message"
|
|
)
|
|
|
|
|
|
class NFOContentResponse(BaseModel):
|
|
"""Response containing NFO XML content.
|
|
|
|
Attributes:
|
|
serie_id: Series identifier
|
|
serie_folder: Series folder name
|
|
content: NFO XML content
|
|
file_size: Size of NFO file in bytes
|
|
last_modified: Last modification timestamp
|
|
"""
|
|
serie_id: str = Field(
|
|
...,
|
|
description="Series identifier"
|
|
)
|
|
serie_folder: str = Field(
|
|
...,
|
|
description="Series folder name"
|
|
)
|
|
content: str = Field(
|
|
...,
|
|
description="NFO XML content"
|
|
)
|
|
file_size: int = Field(
|
|
...,
|
|
description="Size of NFO file in bytes"
|
|
)
|
|
last_modified: Optional[datetime] = Field(
|
|
default=None,
|
|
description="Last modification timestamp"
|
|
)
|
|
|
|
|
|
class MediaDownloadRequest(BaseModel):
|
|
"""Request to download specific media files.
|
|
|
|
Attributes:
|
|
download_poster: Whether to download poster.jpg
|
|
download_logo: Whether to download logo.png
|
|
download_fanart: Whether to download fanart.jpg
|
|
overwrite_existing: Whether to overwrite existing files
|
|
"""
|
|
download_poster: bool = Field(
|
|
default=False,
|
|
description="Whether to download poster.jpg"
|
|
)
|
|
download_logo: bool = Field(
|
|
default=False,
|
|
description="Whether to download logo.png"
|
|
)
|
|
download_fanart: bool = Field(
|
|
default=False,
|
|
description="Whether to download fanart.jpg"
|
|
)
|
|
overwrite_existing: bool = Field(
|
|
default=False,
|
|
description="Whether to overwrite existing files"
|
|
)
|
|
|
|
|
|
class NFOBatchCreateRequest(BaseModel):
|
|
"""Request to batch create NFOs for multiple series.
|
|
|
|
Attributes:
|
|
serie_ids: List of series IDs to process
|
|
download_media: Whether to download media files
|
|
skip_existing: Whether to skip series with existing NFOs
|
|
max_concurrent: Maximum concurrent creations
|
|
"""
|
|
serie_ids: List[str] = Field(
|
|
...,
|
|
description="List of series IDs to process"
|
|
)
|
|
download_media: bool = Field(
|
|
default=True,
|
|
description="Whether to download media files"
|
|
)
|
|
skip_existing: bool = Field(
|
|
default=True,
|
|
description="Whether to skip series with existing NFOs"
|
|
)
|
|
max_concurrent: int = Field(
|
|
default=3,
|
|
ge=1,
|
|
le=10,
|
|
description="Maximum concurrent creations (1-10)"
|
|
)
|
|
|
|
|
|
class NFOBatchResult(BaseModel):
|
|
"""Result for a single series in batch operation.
|
|
|
|
Attributes:
|
|
serie_id: Series identifier
|
|
serie_folder: Series folder name
|
|
success: Whether operation succeeded
|
|
message: Success or error message
|
|
nfo_path: Path to NFO file if successful
|
|
"""
|
|
serie_id: str = Field(
|
|
...,
|
|
description="Series identifier"
|
|
)
|
|
serie_folder: str = Field(
|
|
...,
|
|
description="Series folder name"
|
|
)
|
|
success: bool = Field(
|
|
...,
|
|
description="Whether operation succeeded"
|
|
)
|
|
message: str = Field(
|
|
...,
|
|
description="Success or error message"
|
|
)
|
|
nfo_path: Optional[str] = Field(
|
|
default=None,
|
|
description="Path to NFO file if successful"
|
|
)
|
|
|
|
|
|
class NFOBatchCreateResponse(BaseModel):
|
|
"""Response after batch NFO creation.
|
|
|
|
Attributes:
|
|
total: Total number of series processed
|
|
successful: Number of successful creations
|
|
failed: Number of failed creations
|
|
skipped: Number of skipped series
|
|
results: Detailed results for each series
|
|
"""
|
|
total: int = Field(
|
|
...,
|
|
description="Total number of series processed"
|
|
)
|
|
successful: int = Field(
|
|
...,
|
|
description="Number of successful creations"
|
|
)
|
|
failed: int = Field(
|
|
...,
|
|
description="Number of failed creations"
|
|
)
|
|
skipped: int = Field(
|
|
...,
|
|
description="Number of skipped series"
|
|
)
|
|
results: List[NFOBatchResult] = Field(
|
|
...,
|
|
description="Detailed results for each series"
|
|
)
|
|
|
|
|
|
class NFOMissingSeries(BaseModel):
|
|
"""Information about a series missing NFO.
|
|
|
|
Attributes:
|
|
serie_id: Series identifier
|
|
serie_folder: Series folder name
|
|
serie_name: Display name
|
|
has_media: Whether any media files exist
|
|
media_files: Status of media files
|
|
"""
|
|
serie_id: str = Field(
|
|
...,
|
|
description="Series identifier"
|
|
)
|
|
serie_folder: str = Field(
|
|
...,
|
|
description="Series folder name"
|
|
)
|
|
serie_name: str = Field(
|
|
...,
|
|
description="Display name"
|
|
)
|
|
has_media: bool = Field(
|
|
default=False,
|
|
description="Whether any media files exist"
|
|
)
|
|
media_files: MediaFilesStatus = Field(
|
|
...,
|
|
description="Status of media files"
|
|
)
|
|
|
|
|
|
class NFOMissingResponse(BaseModel):
|
|
"""Response listing series without NFOs.
|
|
|
|
Attributes:
|
|
total_series: Total number of series in library
|
|
missing_nfo_count: Number of series without NFO
|
|
series: List of series missing NFO
|
|
"""
|
|
total_series: int = Field(
|
|
...,
|
|
description="Total number of series in library"
|
|
)
|
|
missing_nfo_count: int = Field(
|
|
...,
|
|
description="Number of series without NFO"
|
|
)
|
|
series: List[NFOMissingSeries] = Field(
|
|
...,
|
|
description="List of series missing NFO"
|
|
)
|
|
|
|
|
|
class NfoSettingsResponse(BaseModel):
|
|
"""Response for NFO settings inspection showing missing required tags."""
|
|
|
|
has_nfo: bool = Field(..., description="Whether tvshow.nfo exists")
|
|
nfo_path: Optional[str] = Field(None, description="Path to NFO file if exists")
|
|
missing_tags: List[str] = Field(
|
|
default_factory=list,
|
|
description="List of missing required tag names"
|
|
)
|
|
required_tags: List[str] = Field(
|
|
default_factory=list,
|
|
description="All required tag names for reference"
|
|
)
|
|
|
|
|
|
class NfoSeriesSettings(BaseModel):
|
|
"""Settings summary for a single series in the needs-repair list."""
|
|
|
|
key: str = Field(..., description="Series unique key")
|
|
name: str = Field(..., description="Series display name")
|
|
folder: str = Field(..., description="Series folder name")
|
|
has_nfo: bool = Field(..., description="Whether tvshow.nfo exists")
|
|
missing_tags: List[str] = Field(
|
|
default_factory=list,
|
|
description="List of missing required tag names"
|
|
)
|
|
tmdb_id: Optional[int] = Field(None, description="TMDB ID if available")
|
|
|
|
|
|
class NfoRepairResponse(BaseModel):
|
|
"""Response after NFO repair attempt."""
|
|
|
|
success: bool = Field(..., description="Whether repair succeeded")
|
|
message: str = Field(..., description="Human-readable result message")
|
|
repaired_tags: List[str] = Field(
|
|
default_factory=list,
|
|
description="Tags that were missing before repair"
|
|
)
|