11from datetime import datetime
2- from typing import List , Optional
2+ from typing import List , Optional , Tuple , Dict , Any
33
44from fastapi import Query , APIRouter
55from pydantic import BaseModel
66from starlette .requests import Request
77
8- search_router = APIRouter ()
8+ from backend .mongo .AnimeDocument import AnimeDocument
9+ from backend .utils .model_utils import DB_CLIENT
910
10- class Anime (BaseModel ):
11- id : int
12- cover_image_url : str
13- title : str
14- genres : List [str ]
15- short_description : str
16- score : float
17- date_added : int
11+ search_router = APIRouter ()
1812
1913class SearchResponse (BaseModel ):
2014 total_count : int
21- shows : List [Anime ]
22-
23- def date_to_millis (date_str : str ) -> int :
24- dt = datetime .strptime (date_str , "%b %d, %Y" )
25- return int (dt .timestamp () * 1000 )
26-
27- # TODO: Remove this and hook up to real database
28- MOCK_ANIME_LIST = [
29- Anime (
30- id = 1 ,
31- cover_image_url = "https://cdn.myanimelist.net/images/anime/1223/96541.jpg" ,
32- title = "Fullmetal Alchemist: Brotherhood" ,
33- genres = ["Action" , "Adventure" , "Drama" , "Fantasy" ],
34- short_description = "Two brothers search for the Philosopher's Stone to restore their bodies after a failed alchemy experiment." ,
35- date_added = date_to_millis ("Apr 5, 2009" ),
36- score = 9.09
37- ),
38- Anime (
39- id = 2 ,
40- cover_image_url = "https://cdn.myanimelist.net/images/anime/1935/127974.jpg" ,
41- title = "Steins;Gate" ,
42- genres = ["Sci-Fi" , "Thriller" , "Drama" ],
43- short_description = "A group of friends accidentally invent a method of sending messages to the past, altering the present." ,
44- date_added = date_to_millis ("Apr 6, 2011" ),
45- score = 9.07
46- ),
47- Anime (
48- id = 3 ,
49- cover_image_url = "https://cdn.myanimelist.net/images/anime/1337/99013.jpg" ,
50- title = "Hunter x Hunter (2011)" ,
51- genres = ["Action" , "Adventure" , "Fantasy" ],
52- short_description = "Gon Freecss aspires to become a Hunter to find his father, meeting friends and facing deadly challenges." ,
53- date_added = date_to_millis ("Oct 2, 2011" ),
54- score = 9.03
55- ),
56- Anime (
57- id = 4 ,
58- cover_image_url = "https://cdn.myanimelist.net/images/anime/10/73274.jpg" ,
59- title = "Gintama" ,
60- genres = ["Action" , "Comedy" , "Sci-Fi" ],
61- short_description = "In an alternate Edo period invaded by aliens, a samurai freelancer takes odd jobs to make ends meet." ,
62- date_added = date_to_millis ("Apr 4, 2006" ),
63- score = 8.94
64- ),
65- Anime (
66- id = 5 ,
67- cover_image_url = "https://cdn.myanimelist.net/images/anime/1000/110531.jpg" ,
68- title = "Attack on Titan Final Season" ,
69- genres = ["Action" , "Drama" , "Fantasy" ],
70- short_description = "The epic conclusion of humanity's battle against the Titans and the truth behind their existence." ,
71- date_added = date_to_millis ("Dec 7, 2020" ),
72- score = 8.79
73- ),
74- Anime (
75- id = 6 ,
76- cover_image_url = "https://cdn.myanimelist.net/images/anime/1295/106551.jpg" ,
77- title = "Kaguya-sama: Love is War" ,
78- genres = ["Comedy" , "Romance" , "School" ],
79- short_description = "Two geniuses at a prestigious academy engage in psychological warfare to make the other confess love first." ,
80- date_added = date_to_millis ("Apr 11, 2020" ),
81- score = 8.41
82- ),
83- Anime (
84- id = 7 ,
85- cover_image_url = "https://cdn.myanimelist.net/images/anime/1500/103005.jpg" ,
86- title = "Vinland Saga" ,
87- genres = ["Action" , "Adventure" , "Drama" , "Historical" ],
88- short_description = "A young Viking seeks revenge against his father's killer while navigating a world of war and slavery." ,
89- date_added = date_to_millis ("Jul 7, 2019" ),
90- score = 8.75
91- ),
92- Anime (
93- id = 8 ,
94- cover_image_url = "https://cdn.myanimelist.net/images/anime/6/86733.jpg" ,
95- title = "Made in Abyss" ,
96- genres = ["Adventure" , "Drama" , "Fantasy" , "Mystery" ],
97- short_description = "An orphan girl and a robot boy descend into a mysterious, perilous chasm to find her mother." ,
98- date_added = date_to_millis ("Jul 7, 2017" ),
99- score = 8.65
100- ),
101- Anime (
102- id = 9 ,
103- cover_image_url = "https://cdn.myanimelist.net/images/anime/5/87048.jpg" ,
104- title = "Your Name." ,
105- genres = ["Drama" , "Romance" , "Supernatural" ],
106- short_description = "Two teenagers swap bodies across time and space, leading to a race against fate." ,
107- date_added = date_to_millis ("Aug 26, 2016" ),
108- score = 8.84
109- ),
110- Anime (
111- id = 10 ,
112- cover_image_url = "https://cdn.myanimelist.net/images/anime/6/79597.jpg" ,
113- title = "Spirited Away" ,
114- genres = ["Adventure" , "Fantasy" , "Supernatural" ],
115- short_description = "A young girl becomes trapped in a spirit world and must work in a bathhouse to free herself and her parents." ,
116- date_added = date_to_millis ("Jul 20, 2001" ),
117- score = 8.77
118- ),
119- ]
15+ shows : List [AnimeDocument ]
16+
17+ def get_mongo_query (
18+ q : Optional [str ] = None ,
19+ genre : Optional [List [str ]] = None ,
20+ year_min : Optional [int ] = None ,
21+ year_max : Optional [int ] = None ,
22+ score_min : Optional [float ] = None ,
23+ score_max : Optional [float ] = None ,
24+ status : Optional [str ] = None ,
25+ idx_from : int = 0 ,
26+ idx_to : int = 19 ,
27+ ) -> Tuple [Dict [str , Any ], int , int ]:
28+ query : Dict [str , Any ] = {}
29+
30+ # Text search
31+ if q :
32+ regex = {"$regex" : q , "$options" : "i" }
33+ query ["$or" ] = [
34+ {"title" : regex },
35+ {"synopsis" : regex },
36+ ]
37+
38+ # Genre filter
39+ if genre :
40+ query ["genres" ] = {"$in" : genre }
41+
42+ # Year range
43+ if year_min is not None or year_max is not None :
44+ year_filter : Dict [str , Any ] = {}
45+ if year_min is not None :
46+ year_filter ["$gte" ] = datetime (year_min , 1 , 1 )
47+ if year_max is not None :
48+ year_filter ["$lt" ] = datetime (year_max + 1 , 1 , 1 )
49+
50+ if year_filter :
51+ query ["date_aired" ] = year_filter
52+
53+ # Score range
54+ if score_min is not None or score_max is not None :
55+ score_filter : Dict [str , Any ] = {}
56+ if score_min is not None :
57+ score_filter ["$gte" ] = score_min
58+ if score_max is not None :
59+ score_filter ["$lte" ] = score_max
60+ if score_filter :
61+ query ["score" ] = score_filter
62+
63+ # Exact status match
64+ if status :
65+ query ["status" ] = status
66+
67+ # Convert idx_from / idx_to to skip + limit
68+ skip = idx_from
69+ limit = max (0 , idx_to - idx_from + 1 )
70+
71+ return query , skip , limit
12072
12173@search_router .get ("/anizenith/search" )
12274async def search (
@@ -133,19 +85,29 @@ async def search(
13385) -> SearchResponse :
13486 """
13587 Search endpoint that returns paginated results.
136- TODO: Integrate with real backend DB queries
13788 """
13889 # Calculate how many items to return in this page
139- total_results = len (MOCK_ANIME_LIST )
140- start = idx_from
141- end = min (idx_to + 1 , total_results )
90+ filter_query , skip , limit = get_mongo_query (
91+ q = q ,
92+ genre = genre ,
93+ year_min = year_min ,
94+ year_max = year_max ,
95+ score_min = score_min ,
96+ score_max = score_max ,
97+ status = status ,
98+ idx_from = idx_from ,
99+ idx_to = idx_to ,
100+ )
101+
102+ # TODO: Remove count documents as it is expensive
103+ total_count = DB_CLIENT .anime_collection .count_documents (filter_query )
104+
105+ # TODO: Convert skip and limit to cursor-based search with $gt
106+ docs = DB_CLIENT .execute_read_query (query = filter_query , skip = skip , limit = limit )
107+ shows = [AnimeDocument (** doc ) for doc in docs ]
142108
143- shows = []
144- for i in range (start , end ):
145- show = MOCK_ANIME_LIST [i ].model_copy ()
146- shows .append (show )
147109
148110 return SearchResponse (
149- total_count = total_results ,
111+ total_count = total_count ,
150112 shows = shows
151113 )
0 commit comments