feat: rescan now saves to database instead of data files

- Update SeriesApp.rescan() to use database storage by default (use_database=True)
- Use SerieScanner.scan_async() for database mode, which saves directly to DB
- Fall back to legacy file-based scan() when use_database=False (for CLI compatibility)
- Reinitialize SerieList from database after scan when in database mode
- Update unit tests to use use_database=False for mocked tests
- Add parameter to control storage mode for backward compatibility
This commit is contained in:
Lukas 2025-12-13 20:37:03 +01:00
parent 3cb644add4
commit 1652f2f6af
2 changed files with 53 additions and 18 deletions

View File

@ -457,14 +457,27 @@ class SeriesApp:
return False
async def rescan(self) -> int:
async def rescan(self, use_database: bool = True) -> int:
"""
Rescan directory for missing episodes (async).
When use_database is True (default), scan results are saved to the
database instead of data files. This is the preferred mode for the
web application.
Args:
use_database: If True, save scan results to database.
If False, use legacy file-based storage (deprecated).
Returns:
Number of series with missing episodes after rescan.
"""
logger.info("Starting directory rescan")
logger.info(
"Starting directory rescan (database mode: %s)",
use_database
)
total_to_scan = 0
try:
# Get total items to scan
@ -507,12 +520,34 @@ class SeriesApp:
)
)
# Perform scan
await asyncio.to_thread(self.serie_scanner.scan, scan_callback)
# Perform scan - use database mode when available
if use_database:
# Import here to avoid circular imports and allow CLI usage
# without database dependencies
from src.server.database.connection import get_db_session
async with get_db_session() as db:
await self.serie_scanner.scan_async(db, scan_callback)
logger.info("Scan results saved to database")
else:
# Legacy file-based mode (deprecated)
await asyncio.to_thread(
self.serie_scanner.scan, scan_callback
)
# Reinitialize list
self.list = SerieList(self.directory_to_search)
await self._init_list()
# Reinitialize list from the appropriate source
if use_database:
from src.server.database.connection import get_db_session
async with get_db_session() as db:
self.list = SerieList(
self.directory_to_search, db_session=db
)
await self.list.load_series_from_db(db)
self.series_list = self.list.GetMissingEpisode()
else:
self.list = SerieList(self.directory_to_search)
await self._init_list()
logger.info("Directory rescan completed successfully")
@ -540,7 +575,7 @@ class SeriesApp:
self._events.scan_status(
ScanStatusEventArgs(
current=0,
total=total_to_scan if 'total_to_scan' in locals() else 0,
total=total_to_scan,
folder="",
status="cancelled",
message="Scan cancelled by user",
@ -555,7 +590,7 @@ class SeriesApp:
self._events.scan_status(
ScanStatusEventArgs(
current=0,
total=total_to_scan if 'total_to_scan' in locals() else 0,
total=total_to_scan,
folder="",
status="failed",
error=e,

View File

@ -240,7 +240,7 @@ class TestSeriesAppReScan:
async def test_rescan_success(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test successful directory rescan."""
"""Test successful directory rescan (file-based mode)."""
test_dir = "/test/anime"
app = SeriesApp(test_dir)
@ -252,8 +252,8 @@ class TestSeriesAppReScan:
app.serie_scanner.reinit = Mock()
app.serie_scanner.scan = Mock()
# Perform rescan
await app.rescan()
# Perform rescan with file-based mode (use_database=False)
await app.rescan(use_database=False)
# Verify rescan completed
app.serie_scanner.reinit.assert_called_once()
@ -266,7 +266,7 @@ class TestSeriesAppReScan:
async def test_rescan_with_callback(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test rescan with progress callbacks."""
"""Test rescan with progress callbacks (file-based mode)."""
test_dir = "/test/anime"
app = SeriesApp(test_dir)
@ -284,8 +284,8 @@ class TestSeriesAppReScan:
app.serie_scanner.scan = Mock(side_effect=mock_scan)
# Perform rescan
await app.rescan()
# Perform rescan with file-based mode (use_database=False)
await app.rescan(use_database=False)
# Verify rescan completed
app.serie_scanner.scan.assert_called_once()
@ -297,7 +297,7 @@ class TestSeriesAppReScan:
async def test_rescan_cancellation(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test rescan cancellation."""
"""Test rescan cancellation (file-based mode)."""
test_dir = "/test/anime"
app = SeriesApp(test_dir)
@ -313,9 +313,9 @@ class TestSeriesAppReScan:
app.serie_scanner.scan = Mock(side_effect=mock_scan)
# Perform rescan - should handle cancellation
# Perform rescan - should handle cancellation (file-based mode)
try:
await app.rescan()
await app.rescan(use_database=False)
except Exception:
pass # Cancellation is expected