The Anime Settings page (src/server/web/static/js/pages/anime-settings.js)
calls GET /api/nfo/{key}/content from its 'View NFO XML' button, but that
endpoint was removed during the NFO refactor (commits 21af502, a8e5487).
The frontend was never updated, so every click on the button 404'd.
Fix:
- Add NfoContentResponse model (key, folder, content, file_size,
last_modified) to src/server/models/nfo.py.
- Add GET /api/nfo/{key}/content handler to src/server/api/nfo.py that
reads <anime_directory>/<folder>/tvshow.nfo and returns it as
{"content": "<xml>", ...} — matching what anime-settings.js
viewNfoContent() already expects (data.content).
- Expose viewNfoContent on AniWorld.AnimeSettingsManager so it is
consistent with the other public methods and directly callable from
tests / other modules.
Tests:
- tests/api/test_nfo_endpoints.py: 4 new tests (auth-required, happy
path returning XML, 404 on unknown series, 404 on missing tvshow.nfo).
Also remove the file-local autouse 'reset_auth' fixture that wiped
the conftest's master-password setup and made any login-based test
fail with a stale-hash 'invalid credentials' error — that fixture
was pre-existing and is a no-op now that conftest.py handles reset.
- tests/frontend/unit/anime_settings.test.js: 3 new tests for
viewNfoContent (URL + auth header, writes <pre>, error toast on
404) and an assertion in the public-API surface test.
422 lines
12 KiB
Python
422 lines
12 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",
|
|
)
|
|
|
|
|
|
class NfoContentResponse(BaseModel):
|
|
"""Response containing the raw contents of a series' tvshow.nfo.
|
|
|
|
Returned by ``GET /api/nfo/{key}/content`` so the Anime Settings page
|
|
can render the XML for the user without exposing the on-disk path to
|
|
the client (only the resolved path is included for display).
|
|
|
|
Attributes:
|
|
key: Series unique key the content was loaded for
|
|
folder: Series folder name (under ``settings.anime_directory``)
|
|
content: Raw XML text of tvshow.nfo (UTF-8)
|
|
file_size: Size of the NFO file in bytes
|
|
last_modified: ISO-8601 timestamp of last on-disk modification
|
|
"""
|
|
|
|
key: str = Field(..., description="Series unique key")
|
|
folder: str = Field(..., description="Series folder name")
|
|
content: str = Field(..., description="Raw XML content of tvshow.nfo")
|
|
file_size: int = Field(..., description="NFO file size in bytes")
|
|
last_modified: datetime = Field(
|
|
..., description="Last modification time of the NFO file"
|
|
)
|