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 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):

View File

@@ -19,6 +19,4 @@ __all__ = [
"SchedulerServiceError",
"get_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")
# 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")

View File

@@ -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",