feat: add folder naming service to fix missing years in anime folder names

Runs after NFO refresh during scheduled rescans. Renames folders that
are missing a year (e.g. 'Naruto' → 'Naruto (1999)') using the year
from the database record.

Safety: _build_target_folder() always strips any existing year suffix
first, preventing double/triple year accumulation like
'Naruto (1999) (1999) (1999)'.

Changes:
- New FolderNamingService (folder_naming_service.py) with safe target
  name construction, DB update, and in-memory cache update
- New SchedulerConfig field: folder_naming_after_nfo_scan (default True)
- Integrated as step 3 in scheduler _perform_rescan() after NFO scan
- Runtime UI: existing 'folder-scan-enabled' checkbox in index.html
  now wired to toggle the feature (app.js + scheduler-config.js)
- Setup screen: new checkbox in setup.html Scheduler Settings section
- API: scheduler config endpoint returns all scan toggles
- Tests: 39 unit tests covering static helpers, rename logic, safety
  guard, and integration cases (folder_naming_service.py)
- Docs: testing guide updated with FolderNamingService examples
This commit is contained in:
2026-07-26 21:45:08 +02:00
parent a384072901
commit 5f46d2e802
9 changed files with 2974 additions and 2447 deletions

View File

@@ -268,6 +268,9 @@ class SchedulerService:
"image_scan_after_rescan": (
self._config.image_scan_after_rescan if self._config else True
),
"folder_naming_after_nfo_scan": (
self._config.folder_naming_after_nfo_scan if self._config else True
),
"last_run": (
self._last_scan_time.isoformat()
if self._last_scan_time
@@ -404,7 +407,18 @@ class SchedulerService:
logger.error("NFO scan failed: %s", exc, exc_info=True)
await self._broadcast("nfo_scan_error", {"error": str(exc)})
# 3. Auto-download (if enabled)
# 3. Folder naming (if enabled, runs after NFO scan)
if self._config and self._config.folder_naming_after_nfo_scan:
if self._config.nfo_scan_after_rescan:
# Only run if NFO scan was also enabled (depends on year in DB)
try:
naming_result = await self._run_folder_naming()
await self._broadcast("folder_naming_completed", naming_result.to_dict())
except Exception as exc:
logger.error("Folder naming failed: %s", exc, exc_info=True)
await self._broadcast("folder_naming_error", {"error": str(exc)})
# 4. Auto-download (if enabled)
if self._config and self._config.auto_download_after_rescan:
try:
queued = await self._run_auto_download()
@@ -413,7 +427,7 @@ class SchedulerService:
logger.error("Auto-download failed: %s", exc, exc_info=True)
await self._broadcast("auto_download_error", {"error": str(exc)})
# 4. Image scan (if enabled)
# 5. Image scan (if enabled)
if self._config and self._config.image_scan_after_rescan:
try:
image_result = await self._run_image_scan()
@@ -536,6 +550,13 @@ class SchedulerService:
)
return result
async def _run_folder_naming(self) -> Any:
"""Run folder naming fix to add missing years to folder names."""
from src.server.services.folder_naming_service import get_folder_naming_service
service = get_folder_naming_service()
logger.info("Starting folder naming scan...")
return await service.run()
async def _run_auto_download(self) -> int:
"""Queue and start downloads for all series with missing episodes."""
from src.server.models.download import EpisodeIdentifier