diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2bf402a..708376c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -54,7 +54,7 @@ This changelog follows [Keep a Changelog](https://keepachangelog.com/) principle - **`NfoRepairService` (`src/core/services/nfo_repair_service.py`)**: New service that detects incomplete `tvshow.nfo` files and triggers TMDB re-fetch. 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 (`src/server/services/initialization_service.py`)**: New async function called during application startup. Iterates every series directory, checks diff --git a/docs/NFO_GUIDE.md b/docs/NFO_GUIDE.md index 4e93cb9..b9b2a33 100644 --- a/docs/NFO_GUIDE.md +++ b/docs/NFO_GUIDE.md @@ -639,33 +639,33 @@ curl -X POST "http://127.0.0.1:8000/api/scheduler/config" \ ## 10. Tag Reference 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()`. -| NFO tag | TMDB source field | Required | -| --------------- | ---------------------------------------------- | -------- | -| `title` | `name` | ✅ | -| `originaltitle` | `original_name` | ✅ | -| `showtitle` | `name` (same as `title`) | ✅ | -| `sorttitle` | `name` (same as `title`) | ✅ | -| `year` | First 4 chars of `first_air_date` | ✅ | -| `plot` | `overview` | ✅ | -| `outline` | `overview` (same as `plot`) | ✅ | -| `tagline` | `tagline` | optional | -| `runtime` | `episode_run_time[0]` | ✅ | -| `premiered` | `first_air_date` | ✅ | -| `status` | `status` | ✅ | -| `mpaa` | US content rating from `content_ratings.results`| optional | -| `fsk` | DE content rating (written as `mpaa` when preferred) | optional | -| `imdbid` | `external_ids.imdb_id` | ✅ | -| `tmdbid` | `id` | ✅ | -| `tvdbid` | `external_ids.tvdb_id` | optional | -| `genre` | `genres[].name` (one element per genre) | ✅ | -| `studio` | `networks[].name` (one element per network) | ✅ | -| `country` | `origin_country[]` or `production_countries[].name` | ✅ | -| `actor` | `credits.cast[]` (top 10, with name/role/thumb)| ✅ | -| `watched` | Always `false` on creation | ✅ | -| `dateadded` | System clock at creation time (`YYYY-MM-DD HH:MM:SS`) | ✅ | +| NFO tag | TMDB source field | Required | +| --------------- | ----------------------------------------------------- | -------- | +| `title` | `name` | ✅ | +| `originaltitle` | `original_name` | ✅ | +| `showtitle` | `name` (same as `title`) | ✅ | +| `sorttitle` | `name` (same as `title`) | ✅ | +| `year` | First 4 chars of `first_air_date` | ✅ | +| `plot` | `overview` | ✅ | +| `outline` | `overview` (same as `plot`) | ✅ | +| `tagline` | `tagline` | optional | +| `runtime` | `episode_run_time[0]` | ✅ | +| `premiered` | `first_air_date` | ✅ | +| `status` | `status` | ✅ | +| `mpaa` | US content rating from `content_ratings.results` | optional | +| `fsk` | DE content rating (written as `mpaa` when preferred) | optional | +| `imdbid` | `external_ids.imdb_id` | ✅ | +| `tmdbid` | `id` | ✅ | +| `tvdbid` | `external_ids.tvdb_id` | optional | +| `genre` | `genres[].name` (one element per genre) | ✅ | +| `studio` | `networks[].name` (one element per network) | ✅ | +| `country` | `origin_country[]` or `production_countries[].name` | ✅ | +| `actor` | `credits.cast[]` (top 10, with name/role/thumb) | ✅ | +| `watched` | Always `false` on creation | ✅ | +| `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 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 `lxml` and checks for the 13 required tags listed below. 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 populated. ### Tags Checked (13 required) -| XPath | Tag name | -| -------------- | --------------- | -| `./title` | `title` | +| XPath | Tag name | +| ----------------- | --------------- | +| `./title` | `title` | | `./originaltitle` | `originaltitle` | -| `./year` | `year` | -| `./plot` | `plot` | -| `./runtime` | `runtime` | -| `./premiered` | `premiered` | -| `./status` | `status` | -| `./imdbid` | `imdbid` | -| `./genre` | `genre` | -| `./studio` | `studio` | -| `./country` | `country` | -| `./actor/name` | `actor/name` | -| `./watched` | `watched` | +| `./year` | `year` | +| `./plot` | `plot` | +| `./runtime` | `runtime` | +| `./premiered` | `premiered` | +| `./status` | `status` | +| `./imdbid` | `imdbid` | +| `./genre` | `genre` | +| `./studio` | `studio` | +| `./country` | `country` | +| `./actor/name` | `actor/name` | +| `./watched` | `watched` | ### Log Messages -| Message | Meaning | -| --- | --- | -| `NFO repair scan complete: 0 of N series queued for repair` | All NFOs are complete — no action needed | +| Message | Meaning | +| ----------------------------------------------------------- | ------------------------------------------------- | +| `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 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: 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` | ### Triggering a Manual Repair @@ -731,11 +731,11 @@ This calls `NFOService.update_tvshow_nfo()` directly and overwrites the existing ### Source Files -| File | Purpose | -| ---- | ------- | -| `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/fastapi_app.py` | Wires `perform_nfo_repair_scan` into the lifespan | +| File | Purpose | +| ----------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `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/fastapi_app.py` | Wires `perform_nfo_repair_scan` into the lifespan | --- diff --git a/src/server/fastapi_app.py b/src/server/fastapi_app.py index 411fc85..02c9157 100644 --- a/src/server/fastapi_app.py +++ b/src/server/fastapi_app.py @@ -219,8 +219,8 @@ async def lifespan(_application: FastAPI): from src.server.services.initialization_service import ( perform_initial_setup, perform_media_scan_if_needed, - perform_nfo_scan_if_needed, perform_nfo_repair_scan, + perform_nfo_scan_if_needed, ) try: diff --git a/tests/integration/test_nfo_repair_startup.py b/tests/integration/test_nfo_repair_startup.py index c7c0f0b..9483349 100644 --- a/tests/integration/test_nfo_repair_startup.py +++ b/tests/integration/test_nfo_repair_startup.py @@ -4,7 +4,7 @@ These tests confirm that: 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. """ -from unittest.mock import AsyncMock, MagicMock, patch, call +from unittest.mock import AsyncMock, MagicMock, call, patch import pytest @@ -15,6 +15,7 @@ class TestNfoRepairScanCalledOnStartup: def test_perform_nfo_repair_scan_imported_in_lifespan(self): """fastapi_app.py lifespan imports perform_nfo_repair_scan.""" import importlib + import src.server.fastapi_app as app_module source = importlib.util.find_spec("src.server.fastapi_app").origin diff --git a/tests/unit/test_initialization_service.py b/tests/unit/test_initialization_service.py index 6d82adc..2c516b0 100644 --- a/tests/unit/test_initialization_service.py +++ b/tests/unit/test_initialization_service.py @@ -27,8 +27,8 @@ from src.server.services.initialization_service import ( _validate_anime_directory, perform_initial_setup, perform_media_scan_if_needed, - perform_nfo_scan_if_needed, perform_nfo_repair_scan, + perform_nfo_scan_if_needed, ) diff --git a/tests/unit/test_nfo_repair_service.py b/tests/unit/test_nfo_repair_service.py index 83de1de..5608e76 100644 --- a/tests/unit/test_nfo_repair_service.py +++ b/tests/unit/test_nfo_repair_service.py @@ -7,8 +7,8 @@ from unittest.mock import AsyncMock, MagicMock import pytest from src.core.services.nfo_repair_service import ( - NfoRepairService, REQUIRED_TAGS, + NfoRepairService, find_missing_tags, nfo_needs_repair, parse_nfo_tags,