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>
This commit is contained in:
2026-06-05 16:33:52 +02:00
parent 7d9f80a0c6
commit 3d33626546
5 changed files with 20 additions and 186 deletions

View File

@@ -1,6 +1,5 @@
import logging import logging
import warnings import warnings
from pathlib import Path
from typing import Any, List, Optional from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, status 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.models.anime import AnimeMetadataUpdate
from src.server.services.anime_service import AnimeService, AnimeServiceError from src.server.services.anime_service import AnimeService, AnimeServiceError
from src.server.services.background_loader_service import BackgroundLoaderService 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 ( from src.server.utils.dependencies import (
get_anime_service, get_anime_service,
get_background_loader_service, get_background_loader_service,
@@ -79,26 +76,14 @@ async def get_anime_status(
class DuplicateFolderGroup(BaseModel): class DuplicateFolderGroup(BaseModel):
"""A group of duplicate folders for the same series. """Placeholder - duplicates functionality removed."""
Attributes:
key: Series key (provider-assigned unique identifier)
folders: List of folder names that are duplicates
folder_count: Number of duplicate folders
"""
key: str = Field(..., description="Series key (unique identifier)") key: str = Field(..., description="Series key (unique identifier)")
folders: List[str] = Field(..., description="List of duplicate folder names") folders: List[str] = Field(..., description="List of duplicate folder names")
folder_count: int = Field(..., description="Number of duplicate folders") folder_count: int = Field(..., description="Number of duplicate folders")
class DuplicateFoldersResponse(BaseModel): class DuplicateFoldersResponse(BaseModel):
"""Response model for duplicate folders listing. """Placeholder - duplicates functionality removed."""
Attributes:
total_groups: Total number of duplicate groups found
duplicate_groups: List of duplicate folder groups
message: Human-readable summary
"""
total_groups: int = Field(..., description="Total number of duplicate groups") total_groups: int = Field(..., description="Total number of duplicate groups")
duplicate_groups: List[DuplicateFolderGroup] = Field( duplicate_groups: List[DuplicateFolderGroup] = Field(
..., description="List of duplicate folder groups" ..., description="List of duplicate folder groups"
@@ -112,64 +97,13 @@ async def get_duplicate_folders(
) -> DuplicateFoldersResponse: ) -> DuplicateFoldersResponse:
"""List all pre-existing duplicate folder groups. """List all pre-existing duplicate folder groups.
Scans the anime directory for folders with tvshow.nfo files that Note: Duplicate folder scanning has been removed. Returns empty response.
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.
""" """
try: return DuplicateFoldersResponse(
if not settings.anime_directory: total_groups=0,
return DuplicateFoldersResponse( duplicate_groups=[],
total_groups=0, message="Duplicate folder scanning has been removed.",
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
class AnimeSummary(BaseModel): class AnimeSummary(BaseModel):

View File

@@ -19,6 +19,4 @@ __all__ = [
"SchedulerServiceError", "SchedulerServiceError",
"get_scheduler_service", "get_scheduler_service",
"reset_scheduler_service", "reset_scheduler_service",
# Sub-services (still in scheduler folder)
"folder_rename_service",
] ]

View File

@@ -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}

View File

@@ -85,19 +85,8 @@ class FolderScanService:
logger.info("NFO repair scan skipped — NFO service removed") logger.info("NFO repair scan skipped — NFO service removed")
# 1.4 — Validate and rename series folders after NFO repair. # 1.4 — Validate and rename series folders after NFO repair.
# Note: folder_rename_service removed - now a stub that does nothing # Note: folder_rename_service removed - skip entirely
logger.info("Starting folder rename validation") logger.info("Folder rename validation skipped — service removed")
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"],
)
# 1.5 — Check and download missing poster.jpg files. # 1.5 — Check and download missing poster.jpg files.
logger.info("Starting poster check") logger.info("Starting poster check")

View File

@@ -107,13 +107,6 @@ class TestRunFolderScanPrerequisites:
"""Scan logs start and completion when prerequisites are met.""" """Scan logs start and completion when prerequisites are met."""
with patch.object( with patch.object(
folder_scan_service, "_prerequisites_met", return_value=True 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( ), patch.object(
folder_scan_service, folder_scan_service,
"check_and_download_missing_posters", "check_and_download_missing_posters",
@@ -147,10 +140,6 @@ class TestNfoRepairIntegration:
"""NFO repair scan is skipped since NFO service removed.""" """NFO repair scan is skipped since NFO service removed."""
with patch.object( with patch.object(
folder_scan_service, "_prerequisites_met", return_value=True 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( ), patch.object(
folder_scan_service, folder_scan_service,
"check_and_download_missing_posters", "check_and_download_missing_posters",
@@ -158,62 +147,30 @@ class TestNfoRepairIntegration:
return_value={"scanned": 0, "downloaded": 0, "skipped": 0, "errors": 0}, return_value={"scanned": 0, "downloaded": 0, "skipped": 0, "errors": 0},
): ):
await folder_scan_service.run_folder_scan() 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: class TestFolderRenameRemoved:
"""Test validate_and_rename_series_folders is called and stats logged.""" """Folder rename validation was removed; scan continues to poster check."""
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calls_folder_rename_service(self, folder_scan_service, tmp_path): async def test_folder_rename_skipped_poster_check_runs(
"""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(
self, folder_scan_service, tmp_path self, folder_scan_service, tmp_path
): ):
"""If validate_and_rename_series_folders raises, the broad except """Folder rename is skipped; scan continues to poster check."""
catches it and the scan stops — poster check is NOT invoked."""
with patch.object( with patch.object(
folder_scan_service, "_prerequisites_met", return_value=True 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( ), patch.object(
folder_scan_service, folder_scan_service,
"check_and_download_missing_posters", "check_and_download_missing_posters",
new_callable=AsyncMock, 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: ) as mock_poster:
await folder_scan_service.run_folder_scan() await folder_scan_service.run_folder_scan()
# Broad except stops the scan; poster check is skipped mock_poster.assert_awaited_once()
mock_poster.assert_not_called()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -536,23 +493,16 @@ class TestRunFolderScanFull:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_full_scan_happy_path(self, folder_scan_service, tmp_path): 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( with patch.object(
folder_scan_service, "_prerequisites_met", return_value=True folder_scan_service, "_prerequisites_met", return_value=True
), patch( ), patch.object(
"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(
folder_scan_service, folder_scan_service,
"check_and_download_missing_posters", "check_and_download_missing_posters",
new_callable=AsyncMock, new_callable=AsyncMock,
return_value={"scanned": 3, "downloaded": 2, "skipped": 1, "errors": 0}, return_value={"scanned": 3, "downloaded": 2, "skipped": 1, "errors": 0},
) as mock_poster: ) as mock_poster:
await folder_scan_service.run_folder_scan() 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() mock_poster.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -560,10 +510,6 @@ class TestRunFolderScanFull:
"""Empty library → all stats zero.""" """Empty library → all stats zero."""
with patch.object( with patch.object(
folder_scan_service, "_prerequisites_met", return_value=True 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( ), patch.object(
folder_scan_service, folder_scan_service,
"check_and_download_missing_posters", "check_and_download_missing_posters",