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:
parent
3cb644add4
commit
1652f2f6af
@ -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,10 +520,32 @@ 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:
|
||||||
|
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 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)
|
self.list = SerieList(self.directory_to_search)
|
||||||
await self._init_list()
|
await self._init_list()
|
||||||
|
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user