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:
@@ -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):
|
||||
|
||||
@@ -19,6 +19,4 @@ __all__ = [
|
||||
"SchedulerServiceError",
|
||||
"get_scheduler_service",
|
||||
"reset_scheduler_service",
|
||||
# Sub-services (still in scheduler folder)
|
||||
"folder_rename_service",
|
||||
]
|
||||
@@ -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}
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user