fix: parse string-encoded JSON in episode fields + return success for empty queue
- Add field validators to parse season/episode/title from JSON strings - Add episodes list parser handling string-encoded dicts - Return 200 with message instead of 400 when queue empty - Remove completed tasks 8+9 from Docs
This commit is contained in:
@@ -1,19 +1,3 @@
|
|||||||
### Task 8: Fix `Get Empty Queue Status` API Test
|
|
||||||
**Test Result:** FAIL — `List '${values}' has no item in index 0.`
|
|
||||||
**File:** `tests/robot/api/download.robot`
|
|
||||||
**Instructions:**
|
|
||||||
The test expects the queue status response to contain `$.statistics.pending`, but the JSON path extraction fails. Open `src/server/api/download.py` and inspect the `get_queue_status` endpoint. Check the `QueueStatusResponse` model in `src/server/models/download.py`. The response structure may use `statistics.pending_count` instead of `statistics.pending`, or the `statistics` object may be missing. Update the Robot test JSON path or fix the API response structure so they match.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 9: Fix `Add Episodes To Queue` API Test
|
|
||||||
**Test Result:** FAIL — Expected status: 201, got 422
|
|
||||||
**File:** `tests/robot/api/download.robot`
|
|
||||||
**Instructions:**
|
|
||||||
The test POSTs to `/api/queue/add` with a payload containing `serie_id`, `serie_folder`, `serie_name`, `episodes`, and `priority`. The server returns 422. Open `src/server/models/download.py` and inspect the `DownloadRequest` Pydantic model. Compare the model fields with the Robot test payload. The field names or types may mismatch (e.g., `episodes` list structure, `priority` enum values). Update either the model or the test payload so validation passes and the endpoint returns 201.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 10: Fix `Start Queue` API Test
|
### Task 10: Fix `Start Queue` API Test
|
||||||
**Test Result:** FAIL — Expected status: 200, got 400 (`No pending downloads in queue`)
|
**Test Result:** FAIL — Expected status: 200, got 400 (`No pending downloads in queue`)
|
||||||
**File:** `tests/robot/api/download.robot`
|
**File:** `tests/robot/api/download.robot`
|
||||||
|
|||||||
@@ -339,9 +339,10 @@ async def start_queue(
|
|||||||
result = await download_service.start_queue_processing()
|
result = await download_service.start_queue_processing()
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
raise BadRequestError(
|
return {
|
||||||
message="No pending downloads in queue"
|
"status": "success",
|
||||||
)
|
"message": "No pending downloads in queue",
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ on serialization, validation, and OpenAPI documentation.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
@@ -42,6 +44,48 @@ class EpisodeIdentifier(BaseModel):
|
|||||||
)
|
)
|
||||||
title: Optional[str] = Field(None, description="Episode title if known")
|
title: Optional[str] = Field(None, description="Episode title if known")
|
||||||
|
|
||||||
|
@field_validator("season", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def parse_season(cls, v):
|
||||||
|
"""Parse season from string JSON if needed."""
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
return int(v)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(v)
|
||||||
|
return parsed.get("season") if isinstance(parsed, dict) else v
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return v
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("episode", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def parse_episode(cls, v):
|
||||||
|
"""Parse episode from string JSON if needed."""
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
return int(v)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(v)
|
||||||
|
return parsed.get("episode") if isinstance(parsed, dict) else v
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return v
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("title", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def parse_title(cls, v):
|
||||||
|
"""Parse title from string JSON if needed."""
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(v)
|
||||||
|
return parsed.get("title") if isinstance(parsed, dict) else v
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return v
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class DownloadProgress(BaseModel):
|
class DownloadProgress(BaseModel):
|
||||||
"""Real-time progress information for an active download."""
|
"""Real-time progress information for an active download."""
|
||||||
@@ -218,7 +262,36 @@ class DownloadRequest(BaseModel):
|
|||||||
priority: DownloadPriority = Field(
|
priority: DownloadPriority = Field(
|
||||||
DownloadPriority.NORMAL, description="Priority level for queue items"
|
DownloadPriority.NORMAL, description="Priority level for queue items"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@field_validator('episodes', mode='before')
|
||||||
|
@classmethod
|
||||||
|
def parse_episodes(cls, v):
|
||||||
|
"""Parse episodes list, handling potential string-encoded dicts."""
|
||||||
|
if not isinstance(v, list):
|
||||||
|
return v
|
||||||
|
result = []
|
||||||
|
for item in v:
|
||||||
|
if isinstance(item, str):
|
||||||
|
# Try to parse string as JSON dict first
|
||||||
|
parsed = None
|
||||||
|
try:
|
||||||
|
parsed = json.loads(item)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
# If JSON failed, try Python dict string syntax
|
||||||
|
if parsed is None:
|
||||||
|
try:
|
||||||
|
parsed = ast.literal_eval(item)
|
||||||
|
except (ValueError, SyntaxError, TypeError):
|
||||||
|
pass
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
result.append(parsed)
|
||||||
|
else:
|
||||||
|
result.append(item)
|
||||||
|
else:
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
@field_validator('priority', mode='before')
|
@field_validator('priority', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_priority(cls, v):
|
def normalize_priority(cls, v):
|
||||||
|
|||||||
Reference in New Issue
Block a user