diff --git a/src/server/api/anime.py b/src/server/api/anime.py index 7cab707..b9b261f 100644 --- a/src/server/api/anime.py +++ b/src/server/api/anime.py @@ -1,7 +1,7 @@ from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, status -from pydantic import BaseModel +from pydantic import BaseModel, Field from src.server.utils.dependencies import ( get_optional_series_app, @@ -101,6 +101,7 @@ class AnimeSummary(BaseModel): site: str # Provider site folder: str # Local folder name missing_episodes: dict # Episode dictionary: {season: [episode_numbers]} + link: Optional[str] = "" # Link to the series page (for adding new series) class Config: """Pydantic model configuration.""" @@ -110,7 +111,8 @@ class AnimeSummary(BaseModel): "name": "Beheneko", "site": "aniworld.to", "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 +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.post( - "/search", - response_model=List[AnimeSummary], - include_in_schema=False, -) -async def search_anime( +async def search_anime_get( query: str, series_app: Optional[Any] = Depends(get_optional_series_app), ) -> List[AnimeSummary]: - """Search the provider for additional series matching a query. + """Search the provider for additional series matching a query (GET). Args: query: Search term passed as query parameter @@ -418,9 +420,48 @@ async def search_anime( Raises: HTTPException: When provider communication fails or query is invalid. - - Note: Authentication removed for input validation testing. - Note: POST method added for compatibility with security tests. + """ + return await _perform_search(query, series_app) + + +@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: # Validate and sanitize the query @@ -444,6 +485,7 @@ async def search_anime( title = match.get("title") or match.get("name") or "" site = match.get("site") or "" folder = match.get("folder") or "" + link = match.get("link") or match.get("url") or "" missing = ( match.get("missing_episodes") or match.get("missing") @@ -454,6 +496,7 @@ async def search_anime( title = getattr(match, "title", getattr(match, "name", "")) site = getattr(match, "site", "") folder = getattr(match, "folder", "") + link = getattr(match, "link", getattr(match, "url", "")) missing = getattr(match, "missing_episodes", {}) summaries.append( @@ -462,6 +505,7 @@ async def search_anime( name=title, site=site, folder=folder, + link=link, missing_episodes=missing, ) ) diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index bdbe2d5..83841e4 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -834,10 +834,13 @@ class AniWorldApp { if (!response) return; 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); } else { - this.showToast(`Search error: ${data.message}`, 'error'); + this.showToast(`Search error: ${data.message || 'Unknown error'}`, 'error'); } } catch (error) { console.error('Search error:', error);