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:
2026-06-14 19:34:58 +02:00
parent 75084b3941
commit 6dc3cda810
11 changed files with 2278 additions and 319 deletions

View File

@@ -133,21 +133,85 @@ class SearchResult(BaseModel):
)
)
title: str = Field(..., description="Series title")
folder: Optional[str] = Field(
None,
description=(
"Series folder name on disk (metadata only) "
"(e.g., 'Attack on Titan (2013)'). For display/filesystem ops only."
)
)
snippet: Optional[str] = Field(None, description="Short description or snippet")
thumbnail: Optional[HttpUrl] = Field(None, description="Thumbnail image URL")
score: Optional[float] = Field(None, ge=0.0, le=1.0, description="Search relevance score (0-1)")
@field_validator('key', mode='before')
@classmethod
def normalize_key(cls, v: str) -> str:
"""Normalize key to lowercase."""
if isinstance(v, str):
return v.lower().strip()
return v
class AnimeDetailsResponse(BaseModel):
"""Detailed response model for a single anime series with all metadata.
Used by the edit modal to pre-fill form fields with existing data.
Attributes:
key: Unique series identifier
name: Display name
folder: Filesystem folder name
year: Release year
status: Show status (Continuing, Ended)
plot: Plot description
genres: List of genres
studio: List of studios
premiered: Premiere date
rating: Rating value (0-10)
rating_votes: Number of votes
tmdb_id: TMDB ID
tvdb_id: TVDB ID
has_nfo: Whether NFO file exists
nfo_created_at: NFO creation timestamp
nfo_updated_at: NFO update timestamp
"""
key: str = Field(..., description="Unique series identifier")
name: str = Field(..., description="Display name")
folder: Optional[str] = Field(None, description="Filesystem folder name")
year: Optional[int] = Field(None, ge=1900, le=2100, description="Release year")
status: Optional[str] = Field(None, description="Show status (Continuing, Ended)")
plot: Optional[str] = Field(None, description="Plot description")
genres: List[str] = Field(default_factory=list, description="List of genres")
studio: List[str] = Field(default_factory=list, description="List of studios")
premiered: Optional[str] = Field(None, description="Premiere date (YYYY-MM-DD)")
rating: Optional[float] = Field(None, ge=0, le=10, description="Rating value (0-10)")
rating_votes: Optional[int] = Field(None, ge=0, description="Number of votes")
tmdb_id: Optional[int] = Field(None, description="TMDB ID")
tvdb_id: Optional[int] = Field(None, description="TVDB ID")
has_nfo: bool = Field(False, description="Whether NFO file exists")
nfo_created_at: Optional[str] = Field(None, description="NFO creation timestamp")
nfo_updated_at: Optional[str] = Field(None, description="NFO update timestamp")
class Config:
json_schema_extra = {
"example": {
"key": "attack-on-titan",
"name": "Attack on Titan",
"folder": "Attack on Titan (2013)",
"year": 2013,
"status": "Ended",
"plot": "Humans fight against giant humanoid Titans.",
"genres": ["Animation", "Action", "Drama"],
"studio": ["Wit Studio", "MAPPA"],
"premiered": "2013-04-07",
"rating": 9.0,
"rating_votes": 500000,
"tmdb_id": 1429,
"tvdb_id": 267440,
"has_nfo": True,
"nfo_created_at": "2025-01-15T10:30:00Z",
"nfo_updated_at": "2025-01-15T10:30:00Z",
}
}
class TMDBSearchResult(BaseModel):
"""TMDB search result for auto-lookup functionality.
Attributes:
tmdb_id: TMDB ID of the matched series
title: Title from TMDB
year: Release year
overview: Short description
vote_average: TMDB rating
"""
tmdb_id: int = Field(..., description="TMDB ID")
title: str = Field(..., description="Title from TMDB")
year: Optional[int] = Field(None, description="Release year")
overview: Optional[str] = Field(None, description="Short description")
vote_average: Optional[float] = Field(None, description="TMDB rating")