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:
2026-01-23 19:11:41 +01:00
parent 5af72c33b8
commit 04f26d5cfc
4 changed files with 165 additions and 105 deletions

View File

@@ -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()