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

@@ -34,11 +34,11 @@ Authorization: Bearer <jwt_token>
**Public Endpoints (no authentication required):**
- `/api/auth/*` - Authentication endpoints
- `/api/health` - Health check endpoints
- `/api/docs`, `/api/redoc` - API documentation
- `/static/*` - Static files
- `/`, `/login`, `/setup`, `/queue` - UI pages
- `/api/auth/*` - Authentication endpoints
- `/api/health` - Health check endpoints
- `/api/docs`, `/api/redoc` - API documentation
- `/static/*` - Static files
- `/`, `/login`, `/setup`, `/queue` - UI pages
Source: [src/server/middleware/auth.py](../src/server/middleware/auth.py#L39-L52)
@@ -91,7 +91,7 @@ Initial setup endpoint to configure the master password. Can only be called once
**Errors:**
- `400 Bad Request` - Master password already configured or invalid password
- `400 Bad Request` - Master password already configured or invalid password
Source: [src/server/api/auth.py](../src/server/api/auth.py#L28-L90)
@@ -120,8 +120,8 @@ Validate master password and return JWT token.
**Errors:**
- `401 Unauthorized` - Invalid credentials
- `429 Too Many Requests` - Account locked due to failed attempts
- `401 Unauthorized` - Invalid credentials
- `429 Too Many Requests` - Account locked due to failed attempts
Source: [src/server/api/auth.py](../src/server/api/auth.py#L93-L124)
@@ -203,7 +203,14 @@ List library series that have missing episodes.
| `page` | int | 1 | Page number (must be positive) |
| `per_page` | int | 20 | Items per page (max 1000) |
| `sort_by` | string | null | Sort field: `title`, `id`, `name`, `missing_episodes` |
| `filter` | string | null | Filter: `no_episodes` (shows only series with no downloaded episodes in folder) |
| `filter` | string | null | Filter: `no_episodes` (shows only series with missing episodes - episodes in DB that haven't been downloaded yet) |
**Filter Details:**
- `no_episodes`: Returns series that have at least one episode in the database with `is_downloaded=False`
- Episodes in the database represent MISSING episodes (from episodeDict during scanning)
- `is_downloaded=False` means the episode file was not found in the folder
- This effectively shows series where no video files were found for missing episodes
**Response (200 OK):**
@@ -221,6 +228,7 @@ List library series that have missing episodes.
```
**Example with filter:**
```bash
GET /api/anime?filter=no_episodes
```
@@ -311,10 +319,10 @@ Add a new series to the library with automatic database persistence, folder crea
**Folder Name Sanitization:**
- Removes invalid filesystem characters: `< > : " / \ | ? *`
- Trims leading/trailing whitespace and dots
- Preserves Unicode characters (for Japanese titles)
- Example: `"Attack on Titan: Final Season"``"Attack on Titan Final Season"`
- Removes invalid filesystem characters: `< > : " / \ | ? *`
- Trims leading/trailing whitespace and dots
- Preserves Unicode characters (for Japanese titles)
- Example: `"Attack on Titan: Final Season"``"Attack on Titan Final Season"`
Source: [src/server/api/anime.py](../src/server/api/anime.py#L604-L710)
@@ -819,8 +827,8 @@ These endpoints manage tvshow.nfo metadata files and associated media (poster, l
**Prerequisites:**
- TMDB API key must be configured in settings
- NFO service returns 503 if API key not configured
- TMDB API key must be configured in settings
- NFO service returns 503 if API key not configured
### GET /api/nfo/{serie_id}/check
@@ -830,7 +838,7 @@ Check if NFO file and media files exist for a series.
**Path Parameters:**
- `serie_id` (string): Series identifier
- `serie_id` (string): Series identifier
**Response (200 OK):**
@@ -853,9 +861,9 @@ Check if NFO file and media files exist for a series.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series not found
- `503 Service Unavailable` - TMDB API key not configured
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series not found
- `503 Service Unavailable` - TMDB API key not configured
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L90-L147)
@@ -867,7 +875,7 @@ Create NFO file and download media for a series.
**Path Parameters:**
- `serie_id` (string): Series identifier
- `serie_id` (string): Series identifier
**Request Body:**
@@ -884,12 +892,12 @@ Create NFO file and download media for a series.
**Fields:**
- `serie_name` (string, optional): Series name for TMDB search (defaults to folder name)
- `year` (integer, optional): Series year to help narrow TMDB search
- `download_poster` (boolean, default: true): Download poster.jpg
- `download_logo` (boolean, default: true): Download logo.png
- `download_fanart` (boolean, default: true): Download fanart.jpg
- `overwrite_existing` (boolean, default: false): Overwrite existing NFO
- `serie_name` (string, optional): Series name for TMDB search (defaults to folder name)
- `year` (integer, optional): Series year to help narrow TMDB search
- `download_poster` (boolean, default: true): Download poster.jpg
- `download_logo` (boolean, default: true): Download logo.png
- `download_fanart` (boolean, default: true): Download fanart.jpg
- `overwrite_existing` (boolean, default: false): Overwrite existing NFO
**Response (200 OK):**
@@ -912,10 +920,10 @@ Create NFO file and download media for a series.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series not found
- `409 Conflict` - NFO already exists (use `overwrite_existing: true`)
- `503 Service Unavailable` - TMDB API error or key not configured
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series not found
- `409 Conflict` - NFO already exists (use `overwrite_existing: true`)
- `503 Service Unavailable` - TMDB API error or key not configured
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L150-L240)
@@ -927,11 +935,11 @@ Update existing NFO file with fresh TMDB data.
**Path Parameters:**
- `serie_id` (string): Series identifier
- `serie_id` (string): Series identifier
**Query Parameters:**
- `download_media` (boolean, default: true): Re-download media files
- `download_media` (boolean, default: true): Re-download media files
**Response (200 OK):**
@@ -954,9 +962,9 @@ Update existing NFO file with fresh TMDB data.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series or NFO not found (use create endpoint)
- `503 Service Unavailable` - TMDB API error
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series or NFO not found (use create endpoint)
- `503 Service Unavailable` - TMDB API error
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L243-L325)
@@ -968,7 +976,7 @@ Get NFO file XML content for a series.
**Path Parameters:**
- `serie_id` (string): Series identifier
- `serie_id` (string): Series identifier
**Response (200 OK):**
@@ -984,8 +992,8 @@ Get NFO file XML content for a series.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series or NFO not found
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series or NFO not found
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L328-L397)
@@ -997,7 +1005,7 @@ Get media files status for a series.
**Path Parameters:**
- `serie_id` (string): Series identifier
- `serie_id` (string): Series identifier
**Response (200 OK):**
@@ -1014,8 +1022,8 @@ Get media files status for a series.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series not found
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series not found
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L400-L447)
@@ -1027,7 +1035,7 @@ Download missing media files for a series.
**Path Parameters:**
- `serie_id` (string): Series identifier
- `serie_id` (string): Series identifier
**Request Body:**
@@ -1054,9 +1062,9 @@ Download missing media files for a series.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series or NFO not found (NFO required for TMDB ID)
- `503 Service Unavailable` - TMDB API error
- `401 Unauthorized` - Not authenticated
- `404 Not Found` - Series or NFO not found (NFO required for TMDB ID)
- `503 Service Unavailable` - TMDB API error
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L450-L519)
@@ -1079,10 +1087,10 @@ Batch create NFO files for multiple series.
**Fields:**
- `serie_ids` (array of strings): Series identifiers to process
- `download_media` (boolean, default: true): Download media files
- `skip_existing` (boolean, default: true): Skip series with existing NFOs
- `max_concurrent` (integer, 1-10, default: 3): Number of concurrent operations
- `serie_ids` (array of strings): Series identifiers to process
- `download_media` (boolean, default: true): Download media files
- `skip_existing` (boolean, default: true): Skip series with existing NFOs
- `max_concurrent` (integer, 1-10, default: 3): Number of concurrent operations
**Response (200 OK):**
@@ -1120,8 +1128,8 @@ Batch create NFO files for multiple series.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `503 Service Unavailable` - TMDB API key not configured
- `401 Unauthorized` - Not authenticated
- `503 Service Unavailable` - TMDB API key not configured
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L522-L634)
@@ -1158,8 +1166,8 @@ Get list of series without NFO files.
**Errors:**
- `401 Unauthorized` - Not authenticated
- `503 Service Unavailable` - TMDB API key not configured
- `401 Unauthorized` - Not authenticated
- `503 Service Unavailable` - TMDB API key not configured
Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L637-L684)
@@ -1352,7 +1360,7 @@ Clients can join/leave rooms to receive specific updates.
**Available Rooms:**
- `downloads` - Download progress and status updates
- `downloads` - Download progress and status updates
### Server Message Format