fix: Correct series filter logic for no_episodes
Critical bug fix: The filter was returning the wrong series because of a misunderstanding of the episode table semantics. ISSUE: - Episodes table contains MISSING episodes (from episodeDict) - is_downloaded=False means episode file not found in folder - Original query logic was backwards - returned series with NO missing episodes instead of series WITH missing episodes SOLUTION: - Simplified query to directly check for episodes with is_downloaded=False - Changed from complex join with count aggregation to simple subquery - Now correctly returns series that have at least one undownloaded episode CHANGES: - src/server/database/service.py: Rewrote get_series_with_no_episodes() method with corrected logic and clearer documentation - tests/unit/test_series_filter.py: Updated test expectations to match corrected behavior with detailed comments explaining episode semantics - docs/API.md: Enhanced documentation explaining filter behavior and episode table meaning TESTS: All 5 unit tests pass with corrected logic
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
"""Tests for series filtering functionality."""
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from src.server.database.models import Base
|
||||
from src.server.database.service import AnimeSeriesService, EpisodeService
|
||||
@@ -61,8 +57,14 @@ async def test_get_series_with_no_episodes_empty_database(
|
||||
async def test_get_series_with_no_episodes_no_downloaded_episodes(
|
||||
async_session: AsyncSession
|
||||
):
|
||||
"""Test that series with no downloaded episodes are returned."""
|
||||
# Create a series with no episodes
|
||||
"""Test that series with no downloaded episodes are returned.
|
||||
|
||||
Episodes in DB represent MISSING episodes, so:
|
||||
- Series with episodes in DB (is_downloaded=False) = no files in folder
|
||||
- Series with no episodes in DB = all episodes downloaded or no info
|
||||
"""
|
||||
# Create a series with NO episodes in DB
|
||||
# (all downloaded or no episodes info)
|
||||
series1 = await AnimeSeriesService.create(
|
||||
async_session,
|
||||
key="test-series-1",
|
||||
@@ -71,7 +73,7 @@ async def test_get_series_with_no_episodes_no_downloaded_episodes(
|
||||
site="https://example.com/test1",
|
||||
)
|
||||
|
||||
# Create a series with undownloaded episodes
|
||||
# Create a series with undownloaded episodes (MISSING - should appear)
|
||||
series2 = await AnimeSeriesService.create(
|
||||
async_session,
|
||||
key="test-series-2",
|
||||
@@ -87,7 +89,7 @@ async def test_get_series_with_no_episodes_no_downloaded_episodes(
|
||||
is_downloaded=False,
|
||||
)
|
||||
|
||||
# Create a series with downloaded episodes (should not be in result)
|
||||
# Create a series with downloaded episodes (should not appear)
|
||||
series3 = await AnimeSeriesService.create(
|
||||
async_session,
|
||||
key="test-series-3",
|
||||
@@ -105,23 +107,26 @@ async def test_get_series_with_no_episodes_no_downloaded_episodes(
|
||||
|
||||
await async_session.commit()
|
||||
|
||||
# Query for series with no downloaded episodes
|
||||
# Query for series with no episodes in folder
|
||||
result = await AnimeSeriesService.get_series_with_no_episodes(
|
||||
async_session
|
||||
)
|
||||
|
||||
# Should return series1 and series2 but not series3
|
||||
# Should return only series2 (has missing episodes)
|
||||
result_ids = {s.id for s in result}
|
||||
assert series1.id in result_ids
|
||||
assert series2.id in result_ids
|
||||
assert series3.id not in result_ids
|
||||
assert series1.id not in result_ids # No episodes in DB
|
||||
assert series2.id in result_ids # Has missing episodes
|
||||
assert series3.id not in result_ids # Has downloaded episodes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_series_with_no_episodes_mixed_downloads(
|
||||
async_session: AsyncSession
|
||||
):
|
||||
"""Test series with mixed downloaded/undownloaded episodes."""
|
||||
"""Test series with mixed downloaded/undownloaded episodes.
|
||||
|
||||
Series with ANY missing episodes (is_downloaded=False) should appear.
|
||||
"""
|
||||
# Create series with some downloaded and some undownloaded episodes
|
||||
series = await AnimeSeriesService.create(
|
||||
async_session,
|
||||
@@ -140,7 +145,7 @@ async def test_get_series_with_no_episodes_mixed_downloads(
|
||||
is_downloaded=True,
|
||||
)
|
||||
|
||||
# Add undownloaded episode
|
||||
# Add undownloaded episode (MISSING)
|
||||
await EpisodeService.create(
|
||||
async_session,
|
||||
series_id=series.id,
|
||||
@@ -151,30 +156,86 @@ async def test_get_series_with_no_episodes_mixed_downloads(
|
||||
|
||||
await async_session.commit()
|
||||
|
||||
# Query for series with no downloaded episodes
|
||||
# Query for series with no episodes in folder
|
||||
result = await AnimeSeriesService.get_series_with_no_episodes(
|
||||
async_session
|
||||
)
|
||||
|
||||
# Should NOT include series with at least one downloaded episode
|
||||
result_ids = {s.id for s in result}
|
||||
assert series.id not in result_ids
|
||||
# Should return the series because it has missing episodes
|
||||
assert len(result) == 1
|
||||
assert result[0].id == series.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_series_with_no_episodes_mixed_seasons(
|
||||
async_session: AsyncSession
|
||||
):
|
||||
"""Test series with some seasons downloaded, some not.
|
||||
|
||||
If ANY episode is still missing (is_downloaded=False), series should appear.
|
||||
"""
|
||||
series = await AnimeSeriesService.create(
|
||||
async_session,
|
||||
key="test-series",
|
||||
name="Test Series",
|
||||
folder="Test Series (2024)",
|
||||
site="https://example.com/test",
|
||||
)
|
||||
|
||||
# Season 1: all episodes downloaded
|
||||
await EpisodeService.create(
|
||||
async_session,
|
||||
series_id=series.id,
|
||||
season=1,
|
||||
episode_number=1,
|
||||
is_downloaded=True,
|
||||
)
|
||||
|
||||
# Season 2: has missing episode
|
||||
await EpisodeService.create(
|
||||
async_session,
|
||||
series_id=series.id,
|
||||
season=2,
|
||||
episode_number=1,
|
||||
is_downloaded=False,
|
||||
)
|
||||
|
||||
await async_session.commit()
|
||||
|
||||
result = await AnimeSeriesService.get_series_with_no_episodes(
|
||||
async_session
|
||||
)
|
||||
|
||||
# Should return the series because season 2 has missing episodes
|
||||
assert len(result) == 1
|
||||
assert result[0].id == series.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_series_with_no_episodes_pagination(
|
||||
async_session: AsyncSession
|
||||
):
|
||||
"""Test pagination works correctly."""
|
||||
# Create multiple series without downloaded episodes
|
||||
"""Test pagination works correctly.
|
||||
|
||||
Note: Series with no episodes in DB won't appear.
|
||||
"""
|
||||
# Create multiple series with missing episodes
|
||||
for i in range(5):
|
||||
await AnimeSeriesService.create(
|
||||
series = await AnimeSeriesService.create(
|
||||
async_session,
|
||||
key=f"test-series-{i}",
|
||||
name=f"Test Series {i}",
|
||||
folder=f"Test Series {i} (2024)",
|
||||
site=f"https://example.com/test{i}",
|
||||
)
|
||||
# Add missing episode
|
||||
await EpisodeService.create(
|
||||
async_session,
|
||||
series_id=series.id,
|
||||
season=1,
|
||||
episode_number=1,
|
||||
is_downloaded=False,
|
||||
)
|
||||
|
||||
await async_session.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user