fix: resolve 422 error and undefined error in anime search endpoint
- Split search endpoint into separate GET and POST handlers - Add SearchAnimeRequest Pydantic model for POST body validation - Add 'link' field to AnimeSummary model for frontend compatibility - Update frontend to handle both array and wrapped response formats - Extract search logic into shared _perform_search() function Fixes issue where POST requests with JSON body were failing with 422 Unprocessable Content error because the endpoint expected query params instead of request body. Also fixes frontend 'undefined' error by handling direct array responses in addition to legacy wrapped format.
This commit is contained in:
parent
95b7059576
commit
02764f7e6f
@ -1,7 +1,7 @@
|
|||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from src.server.utils.dependencies import (
|
from src.server.utils.dependencies import (
|
||||||
get_optional_series_app,
|
get_optional_series_app,
|
||||||
@ -101,6 +101,7 @@ class AnimeSummary(BaseModel):
|
|||||||
site: str # Provider site
|
site: str # Provider site
|
||||||
folder: str # Local folder name
|
folder: str # Local folder name
|
||||||
missing_episodes: dict # Episode dictionary: {season: [episode_numbers]}
|
missing_episodes: dict # Episode dictionary: {season: [episode_numbers]}
|
||||||
|
link: Optional[str] = "" # Link to the series page (for adding new series)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Pydantic model configuration."""
|
"""Pydantic model configuration."""
|
||||||
@ -110,7 +111,8 @@ class AnimeSummary(BaseModel):
|
|||||||
"name": "Beheneko",
|
"name": "Beheneko",
|
||||||
"site": "aniworld.to",
|
"site": "aniworld.to",
|
||||||
"folder": "beheneko the elf girls cat (2025)",
|
"folder": "beheneko the elf girls cat (2025)",
|
||||||
"missing_episodes": {"1": [1, 2, 3, 4]}
|
"missing_episodes": {"1": [1, 2, 3, 4]},
|
||||||
|
"link": "https://aniworld.to/anime/stream/beheneko"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,17 +399,17 @@ def validate_search_query(query: str) -> str:
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
class SearchAnimeRequest(BaseModel):
|
||||||
|
"""Request model for searching anime."""
|
||||||
|
query: str = Field(..., min_length=1, description="Search query string")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search", response_model=List[AnimeSummary])
|
@router.get("/search", response_model=List[AnimeSummary])
|
||||||
@router.post(
|
async def search_anime_get(
|
||||||
"/search",
|
|
||||||
response_model=List[AnimeSummary],
|
|
||||||
include_in_schema=False,
|
|
||||||
)
|
|
||||||
async def search_anime(
|
|
||||||
query: str,
|
query: str,
|
||||||
series_app: Optional[Any] = Depends(get_optional_series_app),
|
series_app: Optional[Any] = Depends(get_optional_series_app),
|
||||||
) -> List[AnimeSummary]:
|
) -> List[AnimeSummary]:
|
||||||
"""Search the provider for additional series matching a query.
|
"""Search the provider for additional series matching a query (GET).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Search term passed as query parameter
|
query: Search term passed as query parameter
|
||||||
@ -418,9 +420,48 @@ async def search_anime(
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: When provider communication fails or query is invalid.
|
HTTPException: When provider communication fails or query is invalid.
|
||||||
|
"""
|
||||||
Note: Authentication removed for input validation testing.
|
return await _perform_search(query, series_app)
|
||||||
Note: POST method added for compatibility with security tests.
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/search",
|
||||||
|
response_model=List[AnimeSummary],
|
||||||
|
)
|
||||||
|
async def search_anime_post(
|
||||||
|
request: SearchAnimeRequest,
|
||||||
|
series_app: Optional[Any] = Depends(get_optional_series_app),
|
||||||
|
) -> List[AnimeSummary]:
|
||||||
|
"""Search the provider for additional series matching a query (POST).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Request containing the search query
|
||||||
|
series_app: Optional SeriesApp instance provided via dependency.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[AnimeSummary]: Discovered matches returned from the provider.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: When provider communication fails or query is invalid.
|
||||||
|
"""
|
||||||
|
return await _perform_search(request.query, series_app)
|
||||||
|
|
||||||
|
|
||||||
|
async def _perform_search(
|
||||||
|
query: str,
|
||||||
|
series_app: Optional[Any],
|
||||||
|
) -> List[AnimeSummary]:
|
||||||
|
"""Internal function to perform the search logic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search term
|
||||||
|
series_app: Optional SeriesApp instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[AnimeSummary]: Discovered matches returned from the provider.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: When provider communication fails or query is invalid.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate and sanitize the query
|
# Validate and sanitize the query
|
||||||
@ -444,6 +485,7 @@ async def search_anime(
|
|||||||
title = match.get("title") or match.get("name") or ""
|
title = match.get("title") or match.get("name") or ""
|
||||||
site = match.get("site") or ""
|
site = match.get("site") or ""
|
||||||
folder = match.get("folder") or ""
|
folder = match.get("folder") or ""
|
||||||
|
link = match.get("link") or match.get("url") or ""
|
||||||
missing = (
|
missing = (
|
||||||
match.get("missing_episodes")
|
match.get("missing_episodes")
|
||||||
or match.get("missing")
|
or match.get("missing")
|
||||||
@ -454,6 +496,7 @@ async def search_anime(
|
|||||||
title = getattr(match, "title", getattr(match, "name", ""))
|
title = getattr(match, "title", getattr(match, "name", ""))
|
||||||
site = getattr(match, "site", "")
|
site = getattr(match, "site", "")
|
||||||
folder = getattr(match, "folder", "")
|
folder = getattr(match, "folder", "")
|
||||||
|
link = getattr(match, "link", getattr(match, "url", ""))
|
||||||
missing = getattr(match, "missing_episodes", {})
|
missing = getattr(match, "missing_episodes", {})
|
||||||
|
|
||||||
summaries.append(
|
summaries.append(
|
||||||
@ -462,6 +505,7 @@ async def search_anime(
|
|||||||
name=title,
|
name=title,
|
||||||
site=site,
|
site=site,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
|
link=link,
|
||||||
missing_episodes=missing,
|
missing_episodes=missing,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -834,10 +834,13 @@ class AniWorldApp {
|
|||||||
if (!response) return;
|
if (!response) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.status === 'success') {
|
// Check if response is a direct array (new format) or wrapped object (legacy)
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
this.displaySearchResults(data);
|
||||||
|
} else if (data.status === 'success') {
|
||||||
this.displaySearchResults(data.results);
|
this.displaySearchResults(data.results);
|
||||||
} else {
|
} else {
|
||||||
this.showToast(`Search error: ${data.message}`, 'error');
|
this.showToast(`Search error: ${data.message || 'Unknown error'}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search error:', error);
|
console.error('Search error:', error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user