diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fcd5e23..51f74fe 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -290,6 +290,9 @@ The FastAPI lifespan function (`src/server/fastapi_app.py`) runs the following s 8. Background loader service started 9. Scheduler service started + +-- Cron-based library rescans configured + +-- Optional: auto-download missing episodes after rescan + +-- Optional: folder maintenance (NFO repair, renaming, poster checks) during scheduled runs 10. NFO repair scan (queue incomplete tvshow.nfo files for background reload) ``` diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 490bbc2..55c3d0a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -117,7 +117,8 @@ Location: `data/config.json` "interval_minutes": 60, "schedule_time": "03:00", "schedule_days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], - "auto_download_after_rescan": false + "auto_download_after_rescan": false, + "folder_scan_enabled": false }, "logging": { "level": "INFO", @@ -173,6 +174,7 @@ Controls automatic cron-based library rescanning (powered by APScheduler). | `scheduler.schedule_time` | string | `"03:00"` | Daily run time in 24-h `HH:MM` format. | | `scheduler.schedule_days` | list[string] | `["mon","tue","wed","thu","fri","sat","sun"]` | Days of the week to run the scan. Empty list disables the cron job. | | `scheduler.auto_download_after_rescan` | bool | `false` | Automatically queue missing episodes for download after each rescan. | +| `scheduler.folder_scan_enabled` | bool | `false` | Run folder maintenance (NFO repair, folder renaming, poster checks) during scheduled runs. | Valid day abbreviations: `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`. diff --git a/docs/tasks.md b/docs/tasks.md index 9af0525..0dafa03 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -1,174 +1,178 @@ -# Tasks — NFO Plot Missing Bug +# Tasks -These tasks fix the root causes of `` being empty in `tvshow.nfo` after adding a series via the web UI. -The bug does **not** appear after a server restart because the repair scan uses a different, correctly isolated code path. +## 1. Scheduled Folder Scan + +### Task 1.1: Add folder scan scheduler configuration + +**Where is that found** +- `src/server/models/config.py` (`SchedulerConfig`) +- `data/config.json` (example/default config) +- `src/server/web/templates/setup.html` (setup UI) +- `src/server/api/auth.py` (config save endpoint, if it validates scheduler fields) + +**Goal. How it should be** +Add a new boolean field `folder_scan_enabled` (default `false`) to `SchedulerConfig`. When `true`, the scheduler will execute the folder maintenance routine during its scheduled run. Add the field to the setup page as a checkbox. Ensure existing configs without this field load successfully (Pydantic default handles this). + +**Possible traps and issues** +- Backward compatibility: old `data/config.json` files must load without errors. Pydantic defaults solve this, but verify by loading an old config. +- The setup page JavaScript must include the new field in the payload sent to `/api/config`. +- Do not confuse this with `auto_download_after_rescan` — this is a separate toggle. + +**Docs changes needed** +- `docs/CONFIGURATION.md`: Document the new `scheduler.folder_scan_enabled` option. +- `docs/ARCHITECTURE.md`: Mention folder scan in the scheduler section. + +**Why this is needed** +Users need an opt-in toggle to enable automatic daily folder maintenance (NFO repair, folder renaming, poster checks) without forcing it on everyone. --- -## Task 1 — Replace shared NFOService in BackgroundLoaderService with per-task instances +### Task 1.2: Create FolderScanService skeleton -- [x] Completed +**Where is that found** +- New file: `src/server/services/folder_scan_service.py` +- `src/server/services/scheduler_service.py` (to call it) -### Where -`src/server/services/background_loader_service.py` — method `_load_nfo_and_images` (~line 555) +**Goal. How it should be** +Create a new `FolderScanService` class with a single async entry point `async def run_folder_scan(self) -> None`. The method should: +1. Log start/completion with structlog. +2. Check prerequisites (`settings.anime_directory` exists, `settings.tmdb_api_key` is set). +3. Skip gracefully with a warning log if prerequisites are missing. +4. Use a module-level semaphore (similar to `_NFO_REPAIR_SEMAPHORE`) to limit concurrent TMDB operations to 3. -```python -nfo_path = await self.series_app.nfo_service.create_tvshow_nfo( - serie_name=task.name, - serie_folder=task.folder, - year=task.year, - ... -) -``` +Keep the implementation empty for the sub-tasks (1.3–1.5) to fill in. Just add the skeleton and the semaphore. -### Goal -Create a fresh, isolated `NFOService` (with its own `TMDBClient` and `aiohttp` session) for every background loading task, exactly the same way `_repair_one_series` in `initialization_service.py` already does it. -Each task must own its client so that closing the session at the end of one task never kills an in-flight request inside another task. +**Possible traps and issues** +- Circular imports: `folder_scan_service.py` will import from `initialization_service`, `config.settings`, etc. Keep imports inside methods or at the bottom if circular issues arise. +- The service should follow the singleton pattern like `SchedulerService` and `DownloadService` if it holds state, or be stateless. For simplicity, make it a plain class instantiated per call or a module-level function set. +- Exception handling: any unhandled exception in the scheduled task should be caught and logged so it doesn't crash the scheduler. -### How it should look -```python -from src.core.services.nfo_factory import NFOServiceFactory +**Docs changes needed** +- `docs/ARCHITECTURE.md`: Add `folder_scan_service.py` to the services list. -factory = NFOServiceFactory() -nfo_service = factory.create() -nfo_path = await nfo_service.create_tvshow_nfo( - serie_name=task.name, - serie_folder=task.folder, - year=task.year, - ... -) -``` - -### Possible traps and issues -- `NFOServiceFactory.create()` raises `ValueError` if no TMDB API key is available. Wrap in try/except and fall back gracefully (same behaviour as now when `nfo_service` is `None`). -- The factory reads the API key from `settings` first, then from `config.json`. Do not pass the key explicitly so the fallback chain stays intact. -- Each new `NFOService` opens its own `aiohttp` connector. Make sure to call `await nfo_service.close()` in a `finally` block to avoid connector leaks. - -### Docs changes needed -None — this is an internal implementation detail. - -### Why this is needed -Up to 5 background workers share one `NFOService`/`TMDBClient` instance. The `async with self.tmdb_client:` context manager inside `create_tvshow_nfo` calls `close()` on `__aexit__`, setting `session = None`. When Worker B exits its context while Worker A is still inside `_enrich_details_with_fallback` trying the `en-US` fallback request, that request throws "Connector is closed". The exception is silently swallowed, both `en-US` and `ja-JP` fallbacks fail, `details["overview"]` stays empty, and `plot` is written as an empty element. +**Why this is needed** +Encapsulates the new daily maintenance logic in its own module, keeping `scheduler_service.py` clean and allowing the folder scan to be tested independently. --- -## Task 2 — Guard NFOService init in SeriesApp on factory fallback, not just env var +### Task 1.3: Integrate NFO repair into folder scan -- [x] Completed +**Where is that found** +- `src/server/services/folder_scan_service.py` +- `src/server/services/initialization_service.py` (`perform_nfo_repair_scan`) -### Where -`src/core/SeriesApp.py` — `__init__` method (~line 175) +**Goal. How it should be** +Inside `FolderScanService.run_folder_scan()`, call `perform_nfo_repair_scan(background_loader=None)` as the first step. Reuse the existing function exactly — do not copy its logic. Log a message before and after the call. -```python -self.nfo_service: Optional[NFOService] = None -if settings.tmdb_api_key: # ← checks env var ONLY - factory = get_nfo_factory() - self.nfo_service = factory.create() -``` +**Possible traps and issues** +- `perform_nfo_repair_scan` spawns `asyncio.create_task` for each repair. When called from the scheduler, these background tasks will still run after `run_folder_scan` returns. This is fine, but log that repairs are queued. +- The function already handles missing `tmdb_api_key` and `anime_directory`, so the caller doesn't need to double-check, but the skeleton from Task 1.2 already checks prerequisites. +- `perform_nfo_repair_scan` imports `nfo_needs_repair` and `NfoRepairService` inside the function, so no heavy import-time dependencies. -### Goal -The guard condition should be equivalent to what `NFOServiceFactory.create()` itself checks: whether the key is available from *any* source (env var or `config.json`). Replace the guard with a try/create pattern so that `nfo_service` is initialised whenever the factory would succeed. +**Docs changes needed** +- `docs/NFO_GUIDE.md`: Update the "Automatic NFO Repair" section to state that repair now runs as part of the scheduled folder scan instead of every startup. -### How it should look -```python -self.nfo_service: Optional[NFOService] = None -try: - from src.core.services.nfo_factory import get_nfo_factory - factory = get_nfo_factory() - self.nfo_service = factory.create() - logger.info("NFO service initialized successfully") -except ValueError: - logger.info("NFO service not available — TMDB API key not configured") -except Exception as e: - logger.warning("Failed to initialize NFO service: %s", e) -``` - -### Possible traps and issues -- This changes the condition from "env var set" to "factory can produce a service". The factory already has a safe fallback and raises `ValueError` when no key exists — so the `except ValueError` path is the normal "not configured" case, not an error. -- `SeriesApp` is used in tests with `settings.tmdb_api_key = None`. Those tests must not be affected; the `except ValueError` branch keeps behaviour identical. -- `series_app.nfo_service` is still `None` when not configured — downstream code that checks `if self.series_app.nfo_service:` remains correct. - -### Docs changes needed -`docs/CONFIGURATION.md` — note that `TMDB_API_KEY` env var is not required if `nfo.tmdb_api_key` is set in `config.json`. - -### Why this is needed -If the TMDB API key is configured only via `config.json` (not the `TMDB_API_KEY` env var), `settings.tmdb_api_key` is `None` and the guard prevents `nfo_service` from ever being created. The background loader then skips NFO creation completely (`nfo_service` is `None`). The repair scan at startup uses `NFOServiceFactory` directly (reads config.json) so it does create the NFO — which is exactly why restart works but add does not. +**Why this is needed** +Reuses the existing, tested NFO repair logic. Moves NFO repair from startup blocking to scheduled background maintenance. --- -## Task 3 — Remove non-reentrant `async with self.tmdb_client:` from NFOService public methods +### Task 1.4: Validate and rename series folders -- [x] Completed +**Where is that found** +- `src/server/services/folder_scan_service.py` +- `src/core/services/nfo_repair_service.py` (for `parse_nfo_tags` or similar NFO parsing) +- `src/server/database/models.py` / `src/server/database/system_settings_service.py` (if folder paths are stored in DB) -### Where -`src/core/services/nfo_service.py` — `create_tvshow_nfo` (~line 151) and `update_tvshow_nfo` (~line 265) +**Goal. How it should be** +After NFO repair, iterate over every subfolder in `settings.anime_directory` that contains a `tvshow.nfo`. For each folder: +1. Parse the NFO to extract `` and `<year>` text values. +2. Compute the expected folder name: `f"{title} ({year})"`. +3. Sanitize the expected name for filesystem safety (remove/replace illegal characters like `/`, `\`, `:`, etc.). +4. Compare with the current folder name (`series_dir.name`). +5. If different, rename the folder using `series_dir.rename(expected_path)`. +6. If the series path is stored in the database (check `anime_service` or DB models), update the database record to point to the new path. -```python -async with self.tmdb_client: - details = await self.tmdb_client.get_tv_show_details(...) - ... -``` +Skip folders where title or year is missing/empty. Log every rename action. -### Goal -The `TMDBClient.__aenter__` / `__aexit__` open and **close** the session, making any concurrent call to the same client instance fail. Because Task 1 creates a fresh instance per call, this context manager becomes redundant. Change both methods to use `_ensure_session()` at the start and `close()` in a `finally` block, or simply call `await self.tmdb_client._ensure_session()` once and close after all requests. This makes the lifetime explicit and prevents double-close if the caller already manages it. +**Possible traps and issues** +- **Database path consistency**: If `Series` or `Episode` models store absolute or relative paths, renaming the folder on disk without updating the DB will break downloads, NFO updates, and the web UI. Must verify whether paths are stored in the DB and update them. +- **Active downloads**: A series currently being downloaded should not be renamed. Check the download queue or lock status before renaming. If no lock mechanism exists, this is a major trap — document it. +- **Filesystem permissions**: The app may not have write permission to the anime directory. Catch `PermissionError` and `OSError` and log gracefully. +- **Special characters**: Titles like `"A / B"` or `"Show: Subtitle"` contain characters illegal in folder names. Define a sanitization function (e.g., replace `/` with `-`, remove trailing dots on Windows, etc.). +- **Duplicate names**: Two different series could sanitize to the same name. Check if target path already exists before renaming. +- **Path length limits**: Very long titles might exceed OS path limits. -### How it should look -```python -async def create_tvshow_nfo(self, ...) -> Path: - try: - await self.tmdb_client._ensure_session() - search_results = await self.tmdb_client.search_tv_show(search_name) - ... - finally: - await self.tmdb_client.close() -``` +**Docs changes needed** +- `docs/NFO_GUIDE.md`: Add a section "Folder Naming Convention" explaining the `<title> (<year>)` format. +- `docs/CONFIGURATION.md`: Mention that enabling folder scan will rename folders. -### Possible traps and issues -- `TMDBClient.close()` is idempotent (checks `session.closed` before closing), so calling it in `finally` is safe even if the try block never opened a session. -- After Task 1 every `NFOService` is short-lived (one call), so `finally: close()` effectively replaces the context manager with no behaviour change. -- Do not remove the `__aenter__`/`__aexit__` from `TMDBClient` itself — other callers (e.g. tests, CLI) may still use it as a context manager. -- `update_tvshow_nfo` has the same pattern; fix both methods. - -### Docs changes needed -None — internal implementation detail. - -### Why this is needed -Even after Task 1 fixes the shared-instance problem, the `async with self.tmdb_client:` pattern is fragile by design: `__aexit__` calls `close()`, which would break any hypothetical future reuse. Removing the implicit close makes the session lifetime explicit and eliminates the root mechanism that caused the original bug. +**Why this is needed** +Enforces a consistent, predictable folder naming scheme across the library, making it easier for media center apps (Kodi, Jellyfin, Plex) to match metadata. --- -## Task 4 — Add `en-US` search fallback so `search_overview` is never empty +### Task 1.5: Check and download missing poster.jpg -### Where -`src/core/services/nfo_service.py` — `create_tvshow_nfo` (~line 178) and `_enrich_details_with_fallback` (~line 395) +**Where is that found** +- `src/server/services/folder_scan_service.py` +- `src/core/utils/image_downloader.py` (`ImageDownloader`) +- `src/core/services/nfo_service.py` or `src/core/services/nfo_repair_service.py` (to get poster URL from NFO or TMDB) -```python -search_overview = tv_show.get("overview") or None # always None for anime — de-DE search returns "" -``` +**Goal. How it should be** +After folder renaming, iterate over series folders again (or combine with Task 1.4 loop). For each folder: +1. Check if `poster.jpg` exists and has a size ≥ `ImageDownloader.min_file_size` (1 KB by default). +2. If missing or too small: + a. Parse `tvshow.nfo` for `<thumb aspect="poster">` or `<thumb>` URL. + b. If no URL in NFO, skip (do not query TMDB again to keep tasks small; the NFO should already have it after repair). + c. Use `ImageDownloader` (with context manager) to download the image to `series_dir / "poster.jpg"`. + d. Validate the downloaded image with `ImageDownloader._validate_image` (or similar existing validation). +3. Use the existing `_NFO_REPAIR_SEMAPHORE` or a new `POSTER_DOWNLOAD_SEMAPHORE` to limit concurrent downloads to 3. -### Goal -When the German `search_tv_show` result has an empty `overview`, perform a second search in `en-US` to obtain a non-empty overview as the last-resort fallback text. Store this as `search_overview` so `_enrich_details_with_fallback` can use it even if all language-specific detail requests fail. +**Possible traps and issues** +- **TMDB rate limiting**: Even downloading images hits TMDB CDN. The semaphore limits concurrency. +- **Invalid images**: A download might produce a 0-byte or corrupted file. `ImageDownloader` already validates with PIL; reuse that. +- **NFO without thumb URL**: If the NFO was created before thumb tags were added, there may be no URL. In that case, skip and log. A future task could query TMDB directly. +- **Write permissions**: Same as Task 1.4. +- **Async session sharing**: `ImageDownloader` manages its own `aiohttp` session. Use `async with ImageDownloader() as downloader:` to ensure cleanup. -### How it should look -```python -search_overview = tv_show.get("overview") or None -if not search_overview: - try: - en_results = await self.tmdb_client.search_tv_show(search_name, language="en-US") - en_match = self._find_best_match(en_results.get("results", []), search_name, year) - search_overview = en_match.get("overview") or None - except Exception: - pass # best-effort only -``` +**Docs changes needed** +- `docs/NFO_GUIDE.md`: Add "Poster Check" subsection under folder scan. +- `docs/CONFIGURATION.md`: Mention that `nfo.download_poster` setting also affects scheduled poster checks. -### Possible traps and issues -- This adds one extra TMDB request per series when the German overview is empty. It is best-effort and must be wrapped in a broad `except` so it never blocks NFO creation. -- The TMDB search endpoint rate-limit is generous; one extra request per add is negligible. -- `_find_best_match` can raise `TMDBAPIError` if the result list is empty — catch both `TMDBAPIError` and `Exception`. -- `update_tvshow_nfo` calls `_enrich_details_with_fallback` without `search_overview`. This is acceptable because the detail request with `en-US` fallback covers it; the search overview is only a last resort for the create path. +**Why this is needed** +Ensures every series has artwork, which is required by most media center front-ends for a polished library view. -### Docs changes needed -None — transparent improvement. +--- -### Why this is needed -Most anime have no German translation on TMDB. The `de-DE` search result returns `overview: ""`. The current code stores this as `search_overview = None` so the last-resort fallback in `_enrich_details_with_fallback` never fires. Combined with session contention (Task 1), the detail-level `en-US` fallback also fails, leaving `plot` empty. This task ensures that at least the search-level `en-US` overview is available as a safety net. +## 2. Remove startup NFO repair + +### Task 2.1: Remove perform_nfo_repair_scan from startup lifespan + +**Where is that found** +- `src/server/fastapi_app.py` (lifespan startup block, lines ~245 and ~319) +- `src/server/services/initialization_service.py` (keep the function, just remove the call site) +- `tests/integration/test_nfo_repair_startup.py` +- `tests/unit/test_initialization_service.py` (tests that call `perform_nfo_repair_scan` directly can stay, but integration tests verifying startup wiring must change) + +**Goal. How it should be** +1. In `src/server/fastapi_app.py`, remove the import of `perform_nfo_repair_scan` from the `initialization_service` import block. +2. Remove the line `await perform_nfo_repair_scan(background_loader)` from the lifespan startup sequence. +3. Update `tests/integration/test_nfo_repair_startup.py`: + - Remove or modify `test_perform_nfo_repair_scan_imported_in_lifespan` and `test_perform_nfo_repair_scan_called_after_media_scan` since the startup wiring is gone. + - Replace with a test that verifies `perform_nfo_repair_scan` is NOT called during startup (or simply delete the file if it has no other purpose). +4. `tests/unit/test_initialization_service.py` tests for `perform_nfo_repair_scan` can remain because they test the function itself, not the startup wiring. + +**Possible traps and issues** +- **Test failures**: `test_nfo_repair_startup.py` will fail immediately after the code change. It must be updated in the same PR. +- **Documentation drift**: `docs/NFO_GUIDE.md`, `docs/CHANGELOG.md`, and `docs/ARCHITECTURE.md` all describe the startup NFO repair behavior. If docs are not updated, users will expect repair on every start. +- **Background loader parameter**: The `background_loader` variable was created partly for `perform_nfo_repair_scan`. After removal, check if `background_loader` is still needed for other startup steps (yes — `perform_media_scan_if_needed` uses it). Do not remove `background_loader` entirely. +- **Import cleanup**: Ensure no unused imports remain in `fastapi_app.py` after removal. + +**Docs changes needed** +- `docs/NFO_GUIDE.md`: Update section 11 "Automatic NFO Repair" to remove startup references and state it runs via scheduler. +- `docs/CHANGELOG.md`: Add an entry under "Changed" or "Removed" noting that startup NFO repair is replaced by scheduled folder scan. +- `docs/ARCHITECTURE.md`: Update the startup sequence description. + +**Why this is needed** +Running `perform_nfo_repair_scan` on every startup slows down server restarts, especially for large libraries. Moving it to a scheduled task keeps startup fast while still ensuring regular maintenance. diff --git a/src/server/api/auth.py b/src/server/api/auth.py index c1717d4..3a3b849 100644 --- a/src/server/api/auth.py +++ b/src/server/api/auth.py @@ -76,6 +76,8 @@ async def setup_auth(req: SetupRequest): config.scheduler.schedule_days = req.scheduler_schedule_days if req.scheduler_auto_download_after_rescan is not None: config.scheduler.auto_download_after_rescan = req.scheduler_auto_download_after_rescan + if req.scheduler_folder_scan_enabled is not None: + config.scheduler.folder_scan_enabled = req.scheduler_folder_scan_enabled # Update logging configuration if req.logging_level: diff --git a/src/server/api/scheduler.py b/src/server/api/scheduler.py index e0e4f8d..5824942 100644 --- a/src/server/api/scheduler.py +++ b/src/server/api/scheduler.py @@ -31,6 +31,7 @@ def _build_response(config: SchedulerConfig) -> Dict[str, Any]: "schedule_time": config.schedule_time, "schedule_days": config.schedule_days, "auto_download_after_rescan": config.auto_download_after_rescan, + "folder_scan_enabled": config.folder_scan_enabled, }, "status": { "is_running": runtime.get("is_running", False), diff --git a/src/server/models/auth.py b/src/server/models/auth.py index 6881002..160be40 100644 --- a/src/server/models/auth.py +++ b/src/server/models/auth.py @@ -73,6 +73,9 @@ class SetupRequest(BaseModel): scheduler_auto_download_after_rescan: Optional[bool] = Field( default=False, description="Auto-download missing episodes after rescan" ) + scheduler_folder_scan_enabled: Optional[bool] = Field( + default=False, description="Run folder maintenance during scheduled run" + ) # Logging configuration logging_level: Optional[str] = Field( diff --git a/src/server/models/config.py b/src/server/models/config.py index a910003..0f14fbf 100644 --- a/src/server/models/config.py +++ b/src/server/models/config.py @@ -39,6 +39,11 @@ class SchedulerConfig(BaseModel): description="Automatically queue and start downloads for all missing " "episodes after a scheduled rescan completes.", ) + folder_scan_enabled: bool = Field( + default=False, + description="Run folder maintenance (NFO repair, folder renaming, " + "poster checks) during the scheduled run.", + ) @field_validator("schedule_time") @classmethod diff --git a/src/server/services/scheduler_service.py b/src/server/services/scheduler_service.py index f409c11..5141e4b 100644 --- a/src/server/services/scheduler_service.py +++ b/src/server/services/scheduler_service.py @@ -145,6 +145,7 @@ class SchedulerService: schedule_time=config.schedule_time, schedule_days=config.schedule_days, auto_download=config.auto_download_after_rescan, + folder_scan=config.folder_scan_enabled, ) if not self._scheduler or not self._scheduler.running: @@ -204,6 +205,9 @@ class SchedulerService: "auto_download_after_rescan": ( self._config.auto_download_after_rescan if self._config else False ), + "folder_scan_enabled": ( + self._config.folder_scan_enabled if self._config else False + ), "last_run": self._last_scan_time.isoformat() if self._last_scan_time else None, "next_run": next_run, "scan_in_progress": self._scan_in_progress, diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index 2ddced2..50ad81f 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -1561,6 +1561,8 @@ class AniWorldApp { document.getElementById('scheduled-rescan-enabled').checked = !!config.enabled; document.getElementById('scheduled-rescan-time').value = config.schedule_time || '03:00'; document.getElementById('auto-download-after-rescan').checked = !!config.auto_download_after_rescan; + const folderScanEl = document.getElementById('folder-scan-enabled'); + if (folderScanEl) folderScanEl.checked = !!config.folder_scan_enabled; // Update day-of-week checkboxes const days = Array.isArray(config.schedule_days) ? config.schedule_days : ['mon','tue','wed','thu','fri','sat','sun']; @@ -1603,6 +1605,8 @@ class AniWorldApp { const enabled = document.getElementById('scheduled-rescan-enabled').checked; const scheduleTime = document.getElementById('scheduled-rescan-time').value || '03:00'; const autoDownload = document.getElementById('auto-download-after-rescan').checked; + const folderScanEl = document.getElementById('folder-scan-enabled'); + const folderScan = folderScanEl ? folderScanEl.checked : false; // Collect checked day-of-week values const scheduleDays = ['mon','tue','wed','thu','fri','sat','sun'] @@ -1618,7 +1622,8 @@ class AniWorldApp { enabled: enabled, schedule_time: scheduleTime, schedule_days: scheduleDays, - auto_download_after_rescan: autoDownload + auto_download_after_rescan: autoDownload, + folder_scan_enabled: folderScan }) }); diff --git a/src/server/web/static/js/index/scheduler-config.js b/src/server/web/static/js/index/scheduler-config.js index a4c07dc..22bbeb6 100644 --- a/src/server/web/static/js/index/scheduler-config.js +++ b/src/server/web/static/js/index/scheduler-config.js @@ -35,6 +35,11 @@ AniWorld.SchedulerConfig = (function() { autoDownload.checked = config.auto_download_after_rescan || false; } + const folderScan = document.getElementById('folder-scan-enabled'); + if (folderScan) { + folderScan.checked = config.folder_scan_enabled || false; + } + // Update schedule day checkboxes const days = config.schedule_days || ['mon','tue','wed','thu','fri','sat','sun']; ['mon','tue','wed','thu','fri','sat','sun'].forEach(function(day) { @@ -82,12 +87,16 @@ AniWorld.SchedulerConfig = (function() { const autoDownloadEl = document.getElementById('auto-download-after-rescan'); const autoDownload = autoDownloadEl ? autoDownloadEl.checked : false; + const folderScanEl = document.getElementById('folder-scan-enabled'); + const folderScan = folderScanEl ? folderScanEl.checked : false; + // POST directly to the scheduler config endpoint const payload = { enabled: enabled, schedule_time: scheduleTime, schedule_days: scheduleDays, - auto_download_after_rescan: autoDownload + auto_download_after_rescan: autoDownload, + folder_scan_enabled: folderScan }; const response = await AniWorld.ApiClient.post(API.SCHEDULER_CONFIG, payload); diff --git a/src/server/web/templates/index.html b/src/server/web/templates/index.html index f7b99fa..5ef9ae1 100644 --- a/src/server/web/templates/index.html +++ b/src/server/web/templates/index.html @@ -309,6 +309,17 @@ </label> </div> + <div class="config-item"> + <label class="checkbox-label"> + <input type="checkbox" id="folder-scan-enabled"> + <span class="checkbox-custom"></span> + <span data-text="folder-scan-enabled">Run folder maintenance (NFO repair, renaming, poster checks)</span> + </label> + <small class="config-hint" data-text="folder-scan-hint"> + Automatically repair NFOs, rename folders, and check posters during scheduled runs. + </small> + </div> + <div class="config-item scheduler-status" id="scheduler-status"> <div class="scheduler-info"> diff --git a/src/server/web/templates/setup.html b/src/server/web/templates/setup.html index b88b62d..b77bf00 100644 --- a/src/server/web/templates/setup.html +++ b/src/server/web/templates/setup.html @@ -479,6 +479,13 @@ <span>Auto-download missing episodes after rescan</span> </label> </div> + <div class="form-group"> + <label class="form-checkbox"> + <input type="checkbox" id="scheduler_folder_scan" name="scheduler_folder_scan"> + <span>Run folder maintenance (NFO repair, renaming, poster checks)</span> + </label> + <div class="form-help">Automatically repair NFOs, rename folders, and check posters during scheduled runs</div> + </div> </div> </div> @@ -761,6 +768,7 @@ scheduler_schedule_time: document.getElementById('scheduler_schedule_time').value || '03:00', scheduler_schedule_days: Array.from(document.querySelectorAll('.scheduler-day-setup-cb:checked')).map(cb => cb.value), scheduler_auto_download_after_rescan: document.getElementById('scheduler_auto_download').checked, + scheduler_folder_scan_enabled: document.getElementById('scheduler_folder_scan').checked, logging_level: document.getElementById('logging_level').value, logging_file: document.getElementById('logging_file').value.trim() || null, logging_max_bytes: document.getElementById('logging_max_bytes').value ?