From 3d336265462114fe939f37e039414216d93005b9 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 5 Jun 2026 16:33:52 +0200 Subject: [PATCH] Remove duplicate folder scanning feature Delete folder_rename_service.py. Stub out get_duplicate_folders API to return empty response. Update folder_scan_service and tests to skip rename step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/server/api/anime.py | 84 ++----------------- src/server/services/scheduler/__init__.py | 2 - .../scheduler/folder_rename_service.py | 33 -------- .../services/scheduler/folder_scan_service.py | 15 +--- tests/unit/test_folder_scan_service.py | 72 ++-------------- 5 files changed, 20 insertions(+), 186 deletions(-) delete mode 100644 src/server/services/scheduler/folder_rename_service.py diff --git a/src/server/api/anime.py b/src/server/api/anime.py index 484c0dd..9cdde35 100644 --- a/src/server/api/anime.py +++ b/src/server/api/anime.py @@ -1,6 +1,5 @@ import logging import warnings -from pathlib import Path from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, status @@ -20,9 +19,7 @@ from src.server.exceptions import ( from src.server.models.anime import AnimeMetadataUpdate from src.server.services.anime_service import AnimeService, AnimeServiceError from src.server.services.background_loader_service import BackgroundLoaderService -from src.server.services.scheduler.folder_rename_service import ( - _scan_for_pre_existing_duplicates, -) + from src.server.utils.dependencies import ( get_anime_service, get_background_loader_service, @@ -79,26 +76,14 @@ async def get_anime_status( class DuplicateFolderGroup(BaseModel): - """A group of duplicate folders for the same series. - - Attributes: - key: Series key (provider-assigned unique identifier) - folders: List of folder names that are duplicates - folder_count: Number of duplicate folders - """ + """Placeholder - duplicates functionality removed.""" key: str = Field(..., description="Series key (unique identifier)") folders: List[str] = Field(..., description="List of duplicate folder names") folder_count: int = Field(..., description="Number of duplicate folders") class DuplicateFoldersResponse(BaseModel): - """Response model for duplicate folders listing. - - Attributes: - total_groups: Total number of duplicate groups found - duplicate_groups: List of duplicate folder groups - message: Human-readable summary - """ + """Placeholder - duplicates functionality removed.""" total_groups: int = Field(..., description="Total number of duplicate groups") duplicate_groups: List[DuplicateFolderGroup] = Field( ..., description="List of duplicate folder groups" @@ -112,64 +97,13 @@ async def get_duplicate_folders( ) -> DuplicateFoldersResponse: """List all pre-existing duplicate folder groups. - Scans the anime directory for folders with tvshow.nfo files that - map to the same series key. Returns groups of duplicates for - manual review and cleanup. - - Returns: - DuplicateFoldersResponse with groups of duplicate folders - - Note: - Not all duplicate folders are safe to merge - some may belong - to different releases (e.g., dubbed vs. subbed). Review carefully - before taking action. + Note: Duplicate folder scanning has been removed. Returns empty response. """ - try: - if not settings.anime_directory: - return DuplicateFoldersResponse( - total_groups=0, - duplicate_groups=[], - message="Anime directory not configured", - ) - - anime_dir = Path(settings.anime_directory) - if not anime_dir.is_dir(): - return DuplicateFoldersResponse( - total_groups=0, - duplicate_groups=[], - message=f"Anime directory not found: {anime_dir}", - ) - - duplicates = _scan_for_pre_existing_duplicates(anime_dir) - - groups = [ - DuplicateFolderGroup( - key=dup.key, - folders=dup.folders, - folder_count=dup.count, - ) - for dup in duplicates - ] - - if groups: - message = ( - f"Found {len(groups)} duplicate group(s). " - "Review carefully - some duplicates may be different releases " - "(e.g., dubbed vs. subbed)." - ) - else: - message = "No duplicate folders found." - - return DuplicateFoldersResponse( - total_groups=len(groups), - duplicate_groups=groups, - message=message, - ) - except Exception as exc: - logger.error("Failed to scan for duplicate folders: %s", str(exc)) - raise ServerError( - message=f"Failed to scan for duplicates: {str(exc)}" - ) from exc + return DuplicateFoldersResponse( + total_groups=0, + duplicate_groups=[], + message="Duplicate folder scanning has been removed.", + ) class AnimeSummary(BaseModel): diff --git a/src/server/services/scheduler/__init__.py b/src/server/services/scheduler/__init__.py index 09f3184..8d0920f 100644 --- a/src/server/services/scheduler/__init__.py +++ b/src/server/services/scheduler/__init__.py @@ -19,6 +19,4 @@ __all__ = [ "SchedulerServiceError", "get_scheduler_service", "reset_scheduler_service", - # Sub-services (still in scheduler folder) - "folder_rename_service", ] \ No newline at end of file diff --git a/src/server/services/scheduler/folder_rename_service.py b/src/server/services/scheduler/folder_rename_service.py deleted file mode 100644 index f5cb822..0000000 --- a/src/server/services/scheduler/folder_rename_service.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Stub module for folder_rename_service (removed).""" - -from typing import Any, Dict, List - - -def _scan_for_pre_existing_duplicates(anime_dir: str) -> List[Any]: - """Stub: returns empty list as folder_rename_service was removed. - - Args: - anime_dir: Unused. - - Returns: - Empty list. - """ - return [] - - -def validate_and_rename_series_folders( - anime_dir: str, - dry_run: bool = False, - background_loader: Any = None -) -> Dict[str, int]: - """Stub: returns empty stats as folder_rename_service was removed. - - Args: - anime_dir: Unused. - dry_run: Unused. - background_loader: Unused. - - Returns: - Empty stats dict. - """ - return {"scanned": 0, "renamed": 0, "skipped": 0, "errors": 0} \ No newline at end of file diff --git a/src/server/services/scheduler/folder_scan_service.py b/src/server/services/scheduler/folder_scan_service.py index 8ff4674..5aad1e6 100644 --- a/src/server/services/scheduler/folder_scan_service.py +++ b/src/server/services/scheduler/folder_scan_service.py @@ -85,19 +85,8 @@ class FolderScanService: logger.info("NFO repair scan skipped — NFO service removed") # 1.4 — Validate and rename series folders after NFO repair. - # Note: folder_rename_service removed - now a stub that does nothing - logger.info("Starting folder rename validation") - from src.server.services.scheduler.folder_rename_service import ( - validate_and_rename_series_folders, - ) - rename_stats = await validate_and_rename_series_folders() - logger.info( - "Folder rename validation complete", - scanned=rename_stats["scanned"], - renamed=rename_stats["renamed"], - skipped=rename_stats["skipped"], - errors=rename_stats["errors"], - ) + # Note: folder_rename_service removed - skip entirely + logger.info("Folder rename validation skipped — service removed") # 1.5 — Check and download missing poster.jpg files. logger.info("Starting poster check") diff --git a/tests/unit/test_folder_scan_service.py b/tests/unit/test_folder_scan_service.py index b751704..86490b7 100644 --- a/tests/unit/test_folder_scan_service.py +++ b/tests/unit/test_folder_scan_service.py @@ -107,13 +107,6 @@ class TestRunFolderScanPrerequisites: """Scan logs start and completion when prerequisites are met.""" with patch.object( folder_scan_service, "_prerequisites_met", return_value=True - ), patch( - "src.server.services.scheduler.folder_scan_service.perform_nfo_repair_scan", - new_callable=AsyncMock, - ), patch( - "src.server.services.scheduler.folder_rename_service.validate_and_rename_series_folders", - new_callable=AsyncMock, - return_value={"scanned": 0, "renamed": 0, "skipped": 0, "errors": 0}, ), patch.object( folder_scan_service, "check_and_download_missing_posters", @@ -147,10 +140,6 @@ class TestNfoRepairIntegration: """NFO repair scan is skipped since NFO service removed.""" with patch.object( folder_scan_service, "_prerequisites_met", return_value=True - ), patch( - "src.server.services.scheduler.folder_rename_service.validate_and_rename_series_folders", - new_callable=AsyncMock, - return_value={"scanned": 0, "renamed": 0, "skipped": 0, "errors": 0}, ), patch.object( folder_scan_service, "check_and_download_missing_posters", @@ -158,62 +147,30 @@ class TestNfoRepairIntegration: return_value={"scanned": 0, "downloaded": 0, "skipped": 0, "errors": 0}, ): await folder_scan_service.run_folder_scan() - # NFO repair is skipped - verify scan continues to folder rename - # No exception means the stub worked correctly # --------------------------------------------------------------------------- -# 1.4 – Folder rename integration +# 1.4 – Folder rename (removed) # --------------------------------------------------------------------------- -class TestFolderRenameIntegration: - """Test validate_and_rename_series_folders is called and stats logged.""" +class TestFolderRenameRemoved: + """Folder rename validation was removed; scan continues to poster check.""" @pytest.mark.asyncio - async def test_calls_folder_rename_service(self, folder_scan_service, tmp_path): - """run_folder_scan must call validate_and_rename_series_folders.""" - with patch.object( - folder_scan_service, "_prerequisites_met", return_value=True - ), patch( - "src.server.services.scheduler.folder_scan_service.perform_nfo_repair_scan", - new_callable=AsyncMock, - ), patch( - "src.server.services.scheduler.folder_rename_service.validate_and_rename_series_folders", - new_callable=AsyncMock, - return_value={"scanned": 5, "renamed": 2, "skipped": 2, "errors": 1}, - ) as mock_rename, patch.object( - folder_scan_service, - "check_and_download_missing_posters", - new_callable=AsyncMock, - return_value={"scanned": 0, "downloaded": 0, "skipped": 0, "errors": 0}, - ): - await folder_scan_service.run_folder_scan() - mock_rename.assert_awaited_once() - - @pytest.mark.asyncio - async def test_folder_rename_failure_does_not_crash_scan( + async def test_folder_rename_skipped_poster_check_runs( self, folder_scan_service, tmp_path ): - """If validate_and_rename_series_folders raises, the broad except - catches it and the scan stops — poster check is NOT invoked.""" + """Folder rename is skipped; scan continues to poster check.""" with patch.object( folder_scan_service, "_prerequisites_met", return_value=True - ), patch( - "src.server.services.scheduler.folder_scan_service.perform_nfo_repair_scan", - new_callable=AsyncMock, - ), patch( - "src.server.services.scheduler.folder_rename_service.validate_and_rename_series_folders", - new_callable=AsyncMock, - side_effect=RuntimeError("rename failed"), ), patch.object( folder_scan_service, "check_and_download_missing_posters", new_callable=AsyncMock, - return_value={"scanned": 0, "downloaded": 0, "skipped": 0, "errors": 0}, + return_value={"scanned": 5, "downloaded": 2, "skipped": 2, "errors": 1}, ) as mock_poster: await folder_scan_service.run_folder_scan() - # Broad except stops the scan; poster check is skipped - mock_poster.assert_not_called() + mock_poster.assert_awaited_once() # --------------------------------------------------------------------------- @@ -536,23 +493,16 @@ class TestRunFolderScanFull: @pytest.mark.asyncio async def test_full_scan_happy_path(self, folder_scan_service, tmp_path): - """All sub-tasks succeed. NFO repair is now a stub.""" + """All sub-tasks succeed. NFO repair and folder rename are stubs.""" with patch.object( folder_scan_service, "_prerequisites_met", return_value=True - ), patch( - "src.server.services.scheduler.folder_rename_service.validate_and_rename_series_folders", - new_callable=AsyncMock, - return_value={"scanned": 3, "renamed": 1, "skipped": 1, "errors": 1}, - ) as mock_rename, patch.object( + ), patch.object( folder_scan_service, "check_and_download_missing_posters", new_callable=AsyncMock, return_value={"scanned": 3, "downloaded": 2, "skipped": 1, "errors": 0}, ) as mock_poster: await folder_scan_service.run_folder_scan() - - # NFO repair is now a stub - not awaited in code - mock_rename.assert_awaited_once() mock_poster.assert_awaited_once() @pytest.mark.asyncio @@ -560,10 +510,6 @@ class TestRunFolderScanFull: """Empty library → all stats zero.""" with patch.object( folder_scan_service, "_prerequisites_met", return_value=True - ), patch( - "src.server.services.scheduler.folder_rename_service.validate_and_rename_series_folders", - new_callable=AsyncMock, - return_value={"scanned": 0, "renamed": 0, "skipped": 0, "errors": 0}, ), patch.object( folder_scan_service, "check_and_download_missing_posters",