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 return False
async def rescan(self) -> int: async def rescan(self, use_database: bool = True) -> int:
""" """
Rescan directory for missing episodes (async). 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: Returns:
Number of series with missing episodes after rescan. 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: try:
# Get total items to scan # Get total items to scan
@ -507,12 +520,34 @@ class SeriesApp:
) )
) )
# Perform scan # Perform scan - use database mode when available
await asyncio.to_thread(self.serie_scanner.scan, scan_callback) 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
# Reinitialize list async with get_db_session() as db:
self.list = SerieList(self.directory_to_search) await self.serie_scanner.scan_async(db, scan_callback)
await self._init_list() 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 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") logger.info("Directory rescan completed successfully")
@ -540,7 +575,7 @@ class SeriesApp:
self._events.scan_status( self._events.scan_status(
ScanStatusEventArgs( ScanStatusEventArgs(
current=0, current=0,
total=total_to_scan if 'total_to_scan' in locals() else 0, total=total_to_scan,
folder="", folder="",
status="cancelled", status="cancelled",
message="Scan cancelled by user", message="Scan cancelled by user",
@ -555,7 +590,7 @@ class SeriesApp:
self._events.scan_status( self._events.scan_status(
ScanStatusEventArgs( ScanStatusEventArgs(
current=0, current=0,
total=total_to_scan if 'total_to_scan' in locals() else 0, total=total_to_scan,
folder="", folder="",
status="failed", status="failed",
error=e, error=e,

View File

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