Skip to content

Commit 7d13b7d

Browse files
committed
Updated code to work with anime_enriched db and backend search
1 parent b554fec commit 7d13b7d

8 files changed

Lines changed: 133 additions & 144 deletions

File tree

‎backend/mongo/AniZenithMongoClient.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def anime_collection(self):
3939
"""
4040
if self._anime_collection is None:
4141
# TODO: Move the hardcoded DB name and collection name into a central Config object
42-
self._anime_collection = self.db_client["anizenith"]["anime"]
42+
self._anime_collection = self.db_client["anizenith"]["anime_enriched"]
4343
return self._anime_collection
4444

4545

@@ -67,12 +67,14 @@ def add_anime(self, anime_document: AnimeDocument) -> None:
6767
self.anime_collection.insert_one(anime_document_dict)
6868

6969

70-
def execute_read_query(self, query, limit = None) -> List[Dict]:
70+
def execute_read_query(self, query, skip = None, limit = None) -> List[Dict]:
7171
try:
7272
if isinstance(query, dict):
7373
# Execute a standard find operation (e.g., {"score": {"$gt": 8.0}})
7474
# "find()" is a standard read-only MongoDB command
7575
cursor = self.anime_collection.find(query)
76+
if skip is not None:
77+
cursor = cursor.skip(skip)
7678
if limit is not None:
7779
cursor = cursor.limit(limit)
7880

‎backend/mongo/AnimeDocument.py‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1+
from datetime import datetime
2+
13
from pydantic import BaseModel
2-
from typing import List
4+
from typing import List, Dict
5+
36

47
# Class to Model a typical Anime Document
58
class AnimeDocument(BaseModel):
6-
name: str
7-
score: float
8-
synopsis: str
9-
genres: List[str]
9+
mal_id: int # ID of anime on MyAnimeList
10+
name: str # English title of anime
11+
alt_titles: Dict[str, str] # { "en": ENGLISH_TITLE, "jp": JAPANESE_TITLE, ...}
12+
score: float # MAL User Mean Score -- Typically 6-10
13+
synopsis: str # Short synopsis of shows, does not contain spoilers beyond first episode
14+
genres: List[str] # Genres list not including demographic genre
15+
demographic: str # Primary demographic
16+
age_rating: str # g | pg | pg-13 | r | r+ | rx
17+
cover_image_url: str # link to MAL image (not CDN)
18+
date_aired: datetime # date aired as datetime (YYYY-MM-DD stored, not hr/mins/secs/ms)
19+
status: str # finished_airing | currently_airing | not_aired
20+
episode_count: int # number of episodes in the anime
21+
avg_episode_len_mins: int # average duration per episode in mins
22+
publishing_company: str # publishing company name | Unknown
23+
recommendations: Dict[str, int] # {"TITLE1":#_OF_RECOMMENDATIONS, "TITLE2":#_OF_RECOMMENDATIONS, ...}
24+
node_name: str # Title of anime (node) -- May not be English, but always matches recommendations title

‎backend/search.py‎

Lines changed: 80 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,74 @@
11
from datetime import datetime
2-
from typing import List, Optional
2+
from typing import List, Optional, Tuple, Dict, Any
33

44
from fastapi import Query, APIRouter
55
from pydantic import BaseModel
66
from 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

1913
class 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")
12274
async 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
)

‎frontend/app.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async def add_security_headers(request: Request, call_next):
7777
f"default-src 'self'; "
7878
f"script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
7979
f"style-src 'self' https://cdnjs.cloudflare.com; "
80-
f"img-src 'self' https://cdn.myanimelist.net data:; "
80+
f"img-src 'self' https://cdn.myanimelist.net https://myanimelist.net data:; "
8181
f"font-src 'self' https://cdnjs.cloudflare.com; "
8282
f"frame-ancestors 'none';"
8383
)

‎frontend/static/css/main.css‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ body {
2525
-ms-user-select: none;
2626
}
2727

28+
.truncate {
29+
display: -webkit-box;
30+
-webkit-box-orient: vertical;
31+
-webkit-line-clamp: 3;
32+
33+
overflow: hidden;
34+
}
35+
2836
/* ===== TOP BAR ===== */
2937
.top-bar {
3038
position: fixed;

‎frontend/static/js/favorites/favorites.js‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const cardTemplate = $('anime-card-template');
2525
async function fetchFavorites() {
2626
try {
2727
// TODO: Remove this and rely solely on browser loading, this is purely for testing
28-
const params = new URLSearchParams({ idx_from: '0', idx_to: '999' });
28+
const params = new URLSearchParams({ idx_from: '0', idx_to: '25' });
2929
const response = await fetch(`${FAVORITES_RETRIEVE_POINT}?${params}`);
3030

3131
if (!response.ok) {
@@ -60,7 +60,7 @@ function saveFavorites(favs) {
6060
async function removeFavorite(animeId) {
6161
const index = favorites.findIndex(a => a.id === animeId);
6262
if (index !== -1) {
63-
const animeTitle = favorites[index].title;
63+
const animeTitle = favorites[index].name;
6464
favorites.splice(index, 1);
6565
saveFavorites(favorites);
6666

@@ -73,8 +73,8 @@ async function removeFavorite(animeId) {
7373

7474
// Lambda expressions for comparing anime favorites stored on browser
7575
const sortFunctions = {
76-
titleAsc: (a, b) => a.title.localeCompare(b.title),
77-
titleDesc: (a, b) => b.title.localeCompare(a.title),
76+
titleAsc: (a, b) => a.name.localeCompare(b.name),
77+
titleDesc: (a, b) => b.name.localeCompare(a.name),
7878
score: (a, b) => (b.score || 0) - (a.score || 0),
7979
dateAdded: (a, b) => (b.date_added || 0) - (a.date_added || 0)
8080
};
@@ -86,7 +86,7 @@ function applyFilters() {
8686
// Filter by search term
8787
if (searchTerm.trim()) {
8888
const term = searchTerm.toLowerCase();
89-
result = result.filter(anime => anime.title.toLowerCase().includes(term));
89+
result = result.filter(anime => anime.name.toLowerCase().includes(term));
9090
}
9191

9292
// Sort using the appropriate function, falling back to dateAdded
@@ -106,19 +106,19 @@ function createAnimeCard(anime) {
106106
const ratingSpan = clone.querySelector('.rating-value');
107107

108108
// Populate the card with anime data
109-
card.dataset.animeId = anime.id;
109+
card.dataset.animeId = anime.mal_id;
110110
img.src = anime.cover_image_url;
111-
img.alt = anime.title;
112-
heartBtn.dataset.id = anime.id;
113-
titleEl.textContent = anime.title;
114-
titleEl.title = anime.title;
111+
img.alt = anime.name;
112+
heartBtn.dataset.id = anime.mal_id;
113+
titleEl.textContent = anime.name;
114+
titleEl.title = anime.name;
115115
ratingSpan.textContent = anime.score?.toFixed(1) ?? 'N/A';
116116

117117
// Hold to remove feature: A long press (800ms) triggers removal, a short press does nothing.
118118
const handlePointerDown = (e) => {
119119
e.preventDefault(); // prevent selecting the text or area behind
120120
heartBtn.classList.add('holding');
121-
heartBtn.holdTimer = setTimeout(() => removeFavorite(anime.id), 800);
121+
heartBtn.holdTimer = setTimeout(() => removeFavorite(anime.mal_id), 800);
122122
};
123123

124124
const handlePointerUp = () => {
@@ -138,7 +138,7 @@ function createAnimeCard(anime) {
138138
// Clicking anywhere on the card (except the heart) navigates to the anime's page
139139
card.addEventListener('click', (e) => {
140140
if (!e.target.closest('.favorite-heart')) {
141-
window.location.href = `/anime/${anime.id}`;
141+
window.location.href = `/anime/${anime.mal_id}`;
142142
}
143143
});
144144

0 commit comments

Comments
 (0)