fix: remove missing episode from DB and memory after download completes

- Fixed _remove_episode_from_missing_list to also update in-memory
  Serie.episodeDict and refresh series_list
- Added _remove_episode_from_memory helper method
- Enhanced logging for download completion and episode removal
- Added 5 unit tests for missing episode removal
This commit is contained in:
2026-02-26 21:02:08 +01:00
parent 624c0db16e
commit b34ee59bca
3 changed files with 363 additions and 19 deletions

View File

@@ -630,3 +630,207 @@ class TestErrorHandling:
download_service._failed_items[0].status == DownloadStatus.FAILED
)
assert download_service._failed_items[0].error is not None
class TestRemoveEpisodeFromMissingList:
"""Test that completed downloads remove episodes from missing list."""
@pytest.mark.asyncio
async def test_remove_episode_from_memory(self, download_service):
"""Test _remove_episode_from_memory updates in-memory state."""
from src.core.entities.series import Serie
# Set up in-memory series with missing episodes
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series (2024)",
episodeDict={1: [1, 2, 3], 2: [1, 2]},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"test-series": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
download_service._anime_service._app = mock_app
# Remove episode S01E02
download_service._remove_episode_from_memory("test-series", 1, 2)
# Episode should be removed from episodeDict
assert 2 not in serie.episodeDict[1]
assert serie.episodeDict[1] == [1, 3]
# Season 2 should be untouched
assert serie.episodeDict[2] == [1, 2]
@pytest.mark.asyncio
async def test_remove_last_episode_in_season_removes_season(
self, download_service
):
"""Test removing the last episode in a season removes the season key."""
from src.core.entities.series import Serie
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series (2024)",
episodeDict={1: [5], 2: [1, 2]},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"test-series": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
download_service._anime_service._app = mock_app
# Remove the only episode in season 1
download_service._remove_episode_from_memory("test-series", 1, 5)
# Season 1 should be completely removed
assert 1 not in serie.episodeDict
# Season 2 untouched
assert serie.episodeDict[2] == [1, 2]
# GetMissingEpisode should have been called to refresh
mock_app.list.GetMissingEpisode.assert_called()
@pytest.mark.asyncio
async def test_remove_episode_unknown_series_no_error(
self, download_service
):
"""Test removing episode for unknown series does not raise."""
mock_app = MagicMock()
mock_app.list.keyDict = {}
download_service._anime_service._app = mock_app
# Should not raise
download_service._remove_episode_from_memory(
"nonexistent-series", 1, 1
)
@pytest.mark.asyncio
async def test_remove_episode_from_missing_list_calls_db_and_memory(
self, download_service
):
"""Test _remove_episode_from_missing_list updates both DB and memory."""
from unittest.mock import patch
from src.core.entities.series import Serie
# Set up in-memory state
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series (2024)",
episodeDict={1: [1, 2, 3]},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"test-series": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
download_service._anime_service._app = mock_app
download_service._anime_service._cached_list_missing = MagicMock()
# Mock DB call
mock_db_session = AsyncMock()
mock_delete = AsyncMock(return_value=True)
with patch(
"src.server.database.connection.get_db_session"
) as mock_get_db, patch(
"src.server.database.service.EpisodeService"
) as mock_ep_svc:
mock_get_db.return_value.__aenter__ = AsyncMock(
return_value=mock_db_session
)
mock_get_db.return_value.__aexit__ = AsyncMock(
return_value=False
)
mock_ep_svc.delete_by_series_and_episode = mock_delete
result = await download_service._remove_episode_from_missing_list(
series_key="test-series",
season=1,
episode=2,
)
# DB deletion was called
mock_delete.assert_awaited_once_with(
db=mock_db_session,
series_key="test-series",
season=1,
episode_number=2,
)
# In-memory update happened
assert 2 not in serie.episodeDict[1]
assert serie.episodeDict[1] == [1, 3]
# Cache was cleared
download_service._anime_service._cached_list_missing.cache_clear.assert_called()
assert result is True
@pytest.mark.asyncio
async def test_download_completion_removes_missing_episode(
self, download_service
):
"""Test full flow: download success removes episode from missing list."""
from unittest.mock import patch
from src.core.entities.series import Serie
# Setup mock anime service to return success
download_service._anime_service.download = AsyncMock(
return_value=True
)
# Set up in-memory series state
serie = Serie(
key="series-1",
name="Test Series",
site="https://example.com",
folder="series",
episodeDict={1: [1, 2, 3]},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"series-1": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
download_service._anime_service._app = mock_app
download_service._anime_service._cached_list_missing = MagicMock()
# Add episode to queue
await download_service.add_to_queue(
serie_id="series-1",
serie_folder="series",
serie_name="Test Series",
episodes=[EpisodeIdentifier(season=1, episode=2)],
)
# Mock DB calls
mock_db_session = AsyncMock()
mock_delete = AsyncMock(return_value=True)
with patch(
"src.server.database.connection.get_db_session"
) as mock_get_db, patch(
"src.server.database.service.EpisodeService"
) as mock_ep_svc:
mock_get_db.return_value.__aenter__ = AsyncMock(
return_value=mock_db_session
)
mock_get_db.return_value.__aexit__ = AsyncMock(
return_value=False
)
mock_ep_svc.delete_by_series_and_episode = mock_delete
# Process the download
item = download_service._pending_queue.popleft()
download_service._pending_items_by_id.pop(item.id, None)
await download_service._process_download(item)
# Episode should be completed
assert len(download_service._completed_items) == 1
assert download_service._completed_items[0].status == DownloadStatus.COMPLETED
# Episode 2 should be removed from in-memory missing list
assert 2 not in serie.episodeDict[1]
assert serie.episodeDict[1] == [1, 3]