style: apply formatter cleanup (import order, whitespace)

This commit is contained in:
2026-02-22 11:26:06 +01:00
parent 8e262c947c
commit 87bf0d71cd
6 changed files with 56 additions and 55 deletions

View File

@@ -54,7 +54,7 @@ This changelog follows [Keep a Changelog](https://keepachangelog.com/) principle
- **`NfoRepairService` (`src/core/services/nfo_repair_service.py`)**: New service - **`NfoRepairService` (`src/core/services/nfo_repair_service.py`)**: New service
that detects incomplete `tvshow.nfo` files and triggers TMDB re-fetch. that detects incomplete `tvshow.nfo` files and triggers TMDB re-fetch.
Provides `parse_nfo_tags()`, `find_missing_tags()`, `nfo_needs_repair()`, and Provides `parse_nfo_tags()`, `find_missing_tags()`, `nfo_needs_repair()`, and
`NfoRepairService.repair_series()`. 13 required tags are checked. `NfoRepairService.repair_series()`. 13 required tags are checked.
- **`perform_nfo_repair_scan()` startup hook - **`perform_nfo_repair_scan()` startup hook
(`src/server/services/initialization_service.py`)**: New async function (`src/server/services/initialization_service.py`)**: New async function
called during application startup. Iterates every series directory, checks called during application startup. Iterates every series directory, checks

View File

@@ -639,33 +639,33 @@ curl -X POST "http://127.0.0.1:8000/api/scheduler/config" \
## 10. Tag Reference ## 10. Tag Reference
The table below lists every XML tag written to `tvshow.nfo` and its source in The table below lists every XML tag written to `tvshow.nfo` and its source in
the TMDB API response. All tags are written whenever the NFO is created or the TMDB API response. All tags are written whenever the NFO is created or
updated via `create_tvshow_nfo()` / `update_tvshow_nfo()`. updated via `create_tvshow_nfo()` / `update_tvshow_nfo()`.
| NFO tag | TMDB source field | Required | | NFO tag | TMDB source field | Required |
| --------------- | ---------------------------------------------- | -------- | | --------------- | ----------------------------------------------------- | -------- |
| `title` | `name` | ✅ | | `title` | `name` | ✅ |
| `originaltitle` | `original_name` | ✅ | | `originaltitle` | `original_name` | ✅ |
| `showtitle` | `name` (same as `title`) | ✅ | | `showtitle` | `name` (same as `title`) | ✅ |
| `sorttitle` | `name` (same as `title`) | ✅ | | `sorttitle` | `name` (same as `title`) | ✅ |
| `year` | First 4 chars of `first_air_date` | ✅ | | `year` | First 4 chars of `first_air_date` | ✅ |
| `plot` | `overview` | ✅ | | `plot` | `overview` | ✅ |
| `outline` | `overview` (same as `plot`) | ✅ | | `outline` | `overview` (same as `plot`) | ✅ |
| `tagline` | `tagline` | optional | | `tagline` | `tagline` | optional |
| `runtime` | `episode_run_time[0]` | ✅ | | `runtime` | `episode_run_time[0]` | ✅ |
| `premiered` | `first_air_date` | ✅ | | `premiered` | `first_air_date` | ✅ |
| `status` | `status` | ✅ | | `status` | `status` | ✅ |
| `mpaa` | US content rating from `content_ratings.results`| optional | | `mpaa` | US content rating from `content_ratings.results` | optional |
| `fsk` | DE content rating (written as `mpaa` when preferred) | optional | | `fsk` | DE content rating (written as `mpaa` when preferred) | optional |
| `imdbid` | `external_ids.imdb_id` | ✅ | | `imdbid` | `external_ids.imdb_id` | ✅ |
| `tmdbid` | `id` | ✅ | | `tmdbid` | `id` | ✅ |
| `tvdbid` | `external_ids.tvdb_id` | optional | | `tvdbid` | `external_ids.tvdb_id` | optional |
| `genre` | `genres[].name` (one element per genre) | ✅ | | `genre` | `genres[].name` (one element per genre) | ✅ |
| `studio` | `networks[].name` (one element per network) | ✅ | | `studio` | `networks[].name` (one element per network) | ✅ |
| `country` | `origin_country[]` or `production_countries[].name` | ✅ | | `country` | `origin_country[]` or `production_countries[].name` | ✅ |
| `actor` | `credits.cast[]` (top 10, with name/role/thumb)| ✅ | | `actor` | `credits.cast[]` (top 10, with name/role/thumb) | ✅ |
| `watched` | Always `false` on creation | ✅ | | `watched` | Always `false` on creation | ✅ |
| `dateadded` | System clock at creation time (`YYYY-MM-DD HH:MM:SS`) | ✅ | | `dateadded` | System clock at creation time (`YYYY-MM-DD HH:MM:SS`) | ✅ |
The mapping logic lives in `src/core/utils/nfo_mapper.py` (`tmdb_to_nfo_model`). The mapping logic lives in `src/core/utils/nfo_mapper.py` (`tmdb_to_nfo_model`).
The XML serialisation lives in `src/core/utils/nfo_generator.py` The XML serialisation lives in `src/core/utils/nfo_generator.py`
@@ -687,36 +687,36 @@ automatically repairs any that are missing required tags.
`src/core/services/nfo_repair_service.py` parses each `tvshow.nfo` with `src/core/services/nfo_repair_service.py` parses each `tvshow.nfo` with
`lxml` and checks for the 13 required tags listed below. `lxml` and checks for the 13 required tags listed below.
3. **Repair** — Series whose NFO is incomplete are queued for background reload 3. **Repair** — Series whose NFO is incomplete are queued for background reload
via `BackgroundLoaderService.add_series_loading_task()`. The background via `BackgroundLoaderService.add_series_loading_task()`. The background
loader re-fetches metadata from TMDB and rewrites the NFO with all tags loader re-fetches metadata from TMDB and rewrites the NFO with all tags
populated. populated.
### Tags Checked (13 required) ### Tags Checked (13 required)
| XPath | Tag name | | XPath | Tag name |
| -------------- | --------------- | | ----------------- | --------------- |
| `./title` | `title` | | `./title` | `title` |
| `./originaltitle` | `originaltitle` | | `./originaltitle` | `originaltitle` |
| `./year` | `year` | | `./year` | `year` |
| `./plot` | `plot` | | `./plot` | `plot` |
| `./runtime` | `runtime` | | `./runtime` | `runtime` |
| `./premiered` | `premiered` | | `./premiered` | `premiered` |
| `./status` | `status` | | `./status` | `status` |
| `./imdbid` | `imdbid` | | `./imdbid` | `imdbid` |
| `./genre` | `genre` | | `./genre` | `genre` |
| `./studio` | `studio` | | `./studio` | `studio` |
| `./country` | `country` | | `./country` | `country` |
| `./actor/name` | `actor/name` | | `./actor/name` | `actor/name` |
| `./watched` | `watched` | | `./watched` | `watched` |
### Log Messages ### Log Messages
| Message | Meaning | | Message | Meaning |
| --- | --- | | ----------------------------------------------------------- | ------------------------------------------------- |
| `NFO repair scan complete: 0 of N series queued for repair` | All NFOs are complete — no action needed | | `NFO repair scan complete: 0 of N series queued for repair` | All NFOs are complete — no action needed |
| `NFO repair scan complete: X of N series queued for repair` | X series had incomplete NFOs and have been queued | | `NFO repair scan complete: X of N series queued for repair` | X series had incomplete NFOs and have been queued |
| `NFO repair scan skipped: TMDB API key not configured` | Set `tmdb_api_key` in `data/config.json` | | `NFO repair scan skipped: TMDB API key not configured` | Set `tmdb_api_key` in `data/config.json` |
| `NFO repair scan skipped: anime directory not configured` | Set `anime_directory` in `data/config.json` | | `NFO repair scan skipped: anime directory not configured` | Set `anime_directory` in `data/config.json` |
### Triggering a Manual Repair ### Triggering a Manual Repair
@@ -731,11 +731,11 @@ This calls `NFOService.update_tvshow_nfo()` directly and overwrites the existing
### Source Files ### Source Files
| File | Purpose | | File | Purpose |
| ---- | ------- | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `src/core/services/nfo_repair_service.py` | `REQUIRED_TAGS`, `parse_nfo_tags`, `find_missing_tags`, `nfo_needs_repair`, `NfoRepairService` | | `src/core/services/nfo_repair_service.py` | `REQUIRED_TAGS`, `parse_nfo_tags`, `find_missing_tags`, `nfo_needs_repair`, `NfoRepairService` |
| `src/server/services/initialization_service.py` | `perform_nfo_repair_scan` startup hook | | `src/server/services/initialization_service.py` | `perform_nfo_repair_scan` startup hook |
| `src/server/fastapi_app.py` | Wires `perform_nfo_repair_scan` into the lifespan | | `src/server/fastapi_app.py` | Wires `perform_nfo_repair_scan` into the lifespan |
--- ---

View File

@@ -219,8 +219,8 @@ async def lifespan(_application: FastAPI):
from src.server.services.initialization_service import ( from src.server.services.initialization_service import (
perform_initial_setup, perform_initial_setup,
perform_media_scan_if_needed, perform_media_scan_if_needed,
perform_nfo_scan_if_needed,
perform_nfo_repair_scan, perform_nfo_repair_scan,
perform_nfo_scan_if_needed,
) )
try: try:

View File

@@ -4,7 +4,7 @@ These tests confirm that:
1. The lifespan calls perform_nfo_repair_scan after perform_media_scan_if_needed. 1. The lifespan calls perform_nfo_repair_scan after perform_media_scan_if_needed.
2. Series with incomplete NFO files are queued via the background_loader. 2. Series with incomplete NFO files are queued via the background_loader.
""" """
from unittest.mock import AsyncMock, MagicMock, patch, call from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest import pytest
@@ -15,6 +15,7 @@ class TestNfoRepairScanCalledOnStartup:
def test_perform_nfo_repair_scan_imported_in_lifespan(self): def test_perform_nfo_repair_scan_imported_in_lifespan(self):
"""fastapi_app.py lifespan imports perform_nfo_repair_scan.""" """fastapi_app.py lifespan imports perform_nfo_repair_scan."""
import importlib import importlib
import src.server.fastapi_app as app_module import src.server.fastapi_app as app_module
source = importlib.util.find_spec("src.server.fastapi_app").origin source = importlib.util.find_spec("src.server.fastapi_app").origin

View File

@@ -27,8 +27,8 @@ from src.server.services.initialization_service import (
_validate_anime_directory, _validate_anime_directory,
perform_initial_setup, perform_initial_setup,
perform_media_scan_if_needed, perform_media_scan_if_needed,
perform_nfo_scan_if_needed,
perform_nfo_repair_scan, perform_nfo_repair_scan,
perform_nfo_scan_if_needed,
) )

View File

@@ -7,8 +7,8 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from src.core.services.nfo_repair_service import ( from src.core.services.nfo_repair_service import (
NfoRepairService,
REQUIRED_TAGS, REQUIRED_TAGS,
NfoRepairService,
find_missing_tags, find_missing_tags,
nfo_needs_repair, nfo_needs_repair,
parse_nfo_tags, parse_nfo_tags,