From 0f04fa40f896432bd58f442fc9da9c8c421a7057 Mon Sep 17 00:00:00 2001 From: harmandeep2993 Date: Sun, 26 Apr 2026 04:23:25 +0200 Subject: [PATCH 1/3] feat: add user history endpoint and update schemas --- api/main.py | 35 +++++++++++++++++++++++++++++++++-- api/schemas.py | 11 ++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/api/main.py b/api/main.py index 5521c9c..406c498 100644 --- a/api/main.py +++ b/api/main.py @@ -6,7 +6,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException -from api.schemas import RecommendationRequest, RecommendationResponse, MovieRecommendation +from api.schemas import RecommendationRequest, RecommendationResponse, MovieRecommendation, HistoryResponse from api.services import get_recommendations from src.data import load_dataset from src.utils import get_logger @@ -22,6 +22,7 @@ async def lifespan(app: FastAPI): logger.info("Loading datasets at startup...") data = load_dataset() datasets["movies"] = data["movies"] + datasets["ratings"] = data["ratings"] logger.info("Datasets loaded successfully") yield logger.info("Shutting down API...") @@ -61,4 +62,34 @@ def recommend(request: RecommendationRequest): recommendations=[ MovieRecommendation(**r) for r in recommendations ] - ) \ No newline at end of file + ) + +@app.get("/user/{user_id}/history", response_model=HistoryResponse) +def get_user_history(user_id: int): + + movies = datasets.get("movies") + ratings = datasets.get("ratings") + + if movies is None or ratings is None: + raise HTTPException(status_code=500, detail="Datasets not loaded") + + # get user ratings + user_ratings = ratings[ratings["user_id"] == user_id] + + if user_ratings.empty: + raise HTTPException(status_code=404, detail=f"User {user_id} not found") + + # get top rated movies + top_rated = user_ratings.sort_values("rating", ascending=False).head(10) + + history = [] + for _, row in top_rated.iterrows(): + title = movies[movies["movie_id"] == row["movie_id"]]["title"].values + if len(title) > 0: + history.append({ + "movie_id": int(row["movie_id"]), + "title": title[0], + "rating": float(row["rating"]) + }) + + return HistoryResponse(user_id=user_id, history=history) \ No newline at end of file diff --git a/api/schemas.py b/api/schemas.py index c6f04f6..1dc1961 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -20,4 +20,13 @@ class MovieRecommendation(BaseModel): class RecommendationResponse(BaseModel): """Schema for recommendation response.""" user_id: int - recommendations: List[MovieRecommendation] \ No newline at end of file + recommendations: List[MovieRecommendation] + +class WatchedMovie(BaseModel): + movie_id: int + title: str + rating: float + +class HistoryResponse(BaseModel): + user_id: int + history: List[WatchedMovie] \ No newline at end of file From 9fd754d5dfdaf9db5218cb434ff0147dfae86ab8 Mon Sep 17 00:00:00 2001 From: harmandeep2993 Date: Sun, 26 Apr 2026 04:23:41 +0200 Subject: [PATCH 2/3] feat: redesign frontend with CinePick dark UI and TMDB posters --- frontend/app.py | 326 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 308 insertions(+), 18 deletions(-) diff --git a/frontend/app.py b/frontend/app.py index 0601095..ff7c7fe 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -1,32 +1,322 @@ -#frontend/app.py """ -Streamlit frontend for movie recommendation system. +Streamlit frontend for CinePick- Movie Recommendation System. """ -import streamlit as st +import os import requests +import streamlit as st +from dotenv import load_dotenv + +load_dotenv() API_URL = "http://127.0.0.1:8000" +TMDB_API_KEY = os.getenv("TMDB_API_KEY") +TMDB_BASE_URL = "https://api.themoviedb.org/3" +TMDB_IMAGE_URL = "https://image.tmdb.org/t/p/w500" -st.title("๐ŸŽฌ Movie Recommendation System") -st.write("Get personalized movie recommendations!") +st.set_page_config( + page_title="CinePick", + page_icon="๐Ÿฟ", + layout="wide", + initial_sidebar_state="expanded" +) -user_id = st.number_input("Enter User ID", min_value=1, max_value=6040, value=1) -n = st.slider("Number of recommendations", min_value=1, max_value=20, value=10) +st.markdown(""" + +""", unsafe_allow_html=True) -if st.button("Get Recommendations"): - with st.spinner("Getting recommendations..."): + +@st.cache_data(ttl=300) +def get_recommendations(user_id: int, n: int) -> list: + try: response = requests.post( f"{API_URL}/recommendations", json={"user_id": user_id, "n": n} ) - if response.status_code == 200: - data = response.json() - recommendations = data["recommendations"] - st.subheader(f"Top {n} recommendations for User {user_id}:") - - for i, movie in enumerate(recommendations, 1): - st.write(f"{i}. **{movie['title']}** (score: {movie['predicted_score']})") - else: - st.error(f"Error {response.status_code}: {response.text}") \ No newline at end of file + return response.json()["recommendations"] + return [] + except Exception as e: + st.error(f"API error: {e}") + return [] + + +@st.cache_data(ttl=300) +def get_user_history(user_id: int) -> list: + try: + response = requests.get(f"{API_URL}/user/{user_id}/history") + if response.status_code == 200: + return response.json()["history"] + return [] + except Exception: + return [] + + +@st.cache_data(ttl=3600) +def search_tmdb(title: str) -> dict: + try: + clean_title = title.split("(")[0].strip() + response = requests.get( + f"{TMDB_BASE_URL}/search/movie", + params={ + "api_key": TMDB_API_KEY, + "query": clean_title, + "language": "en-US", + "page": 1 + } + ) + if response.status_code == 200: + results = response.json().get("results", []) + if results: + return results[0] + except Exception: + pass + return {} + + +def get_poster_url(tmdb_data: dict) -> str: + poster_path = tmdb_data.get("poster_path") + if poster_path: + return f"{TMDB_IMAGE_URL}{poster_path}" + return None + + +# โ”€โ”€ sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +with st.sidebar: + st.markdown(""" +
+

๐Ÿฟ

+

CinePick

+

discover what to watch next

+
+ """, unsafe_allow_html=True) + + st.markdown("---") + + st.markdown("

User ID

", unsafe_allow_html=True) + user_id = st.number_input( + "", + min_value=1, + max_value=6040, + value=1, + step=1, + label_visibility="collapsed" + ) + st.markdown("

Valid range: 1 โ€” 6040

", unsafe_allow_html=True) + + st.markdown("

Recommendations

", unsafe_allow_html=True) + n = st.select_slider( + "", + options=[5, 10, 20], + value=10, + label_visibility="collapsed" + ) + + st.markdown("
", unsafe_allow_html=True) + get_recs = st.button("๐Ÿฟ Get Recommendations") + + st.markdown("---") + + st.markdown("

About

", unsafe_allow_html=True) + + stats = [ + ("Algorithm", "SVD", "#e0e0e0"), + ("RMSE", "0.965", "#4CAF50"), + ("Dataset", "MovieLens 1M", "#e0e0e0"), + ("Users", "6,040", "#e0e0e0"), + ("Ratings", "1M+", "#e0e0e0"), + ] + for label, value, color in stats: + st.markdown(f""" +
+ {label} + {value} +
+ """, unsafe_allow_html=True) + +# โ”€โ”€ main content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +if get_recs: + + # hero banner + st.markdown(f""" +
+
+

Welcome back, User {user_id} ๐Ÿ‘‹

+

Powered by SVD collaborative filtering ยท MovieLens 1M ยท RMSE 0.965

+
+
+

{n}

+

recommendations

+
+
+ """, unsafe_allow_html=True) + + # recommendations section + st.markdown(""" +
+
+

Top picks for you

+
+ """, unsafe_allow_html=True) + + with st.spinner("Finding movies you will love..."): + recommendations = get_recommendations(user_id, n) + + if recommendations: + cols = st.columns(5) + for i, movie in enumerate(recommendations): + with cols[i % 5]: + tmdb_data = search_tmdb(movie["title"]) + poster_url = get_poster_url(tmdb_data) + + if poster_url: + st.image(poster_url, use_container_width=True) + else: + st.markdown( + "
No poster
", + unsafe_allow_html=True + ) + + st.markdown(f"

{movie['title']}

", unsafe_allow_html=True) + st.markdown(f"

{movie['predicted_score']} / 5

", unsafe_allow_html=True) + + tmdb_rating = tmdb_data.get("vote_average") + if tmdb_rating: + st.markdown(f"

TMDB {tmdb_rating:.1f}

", unsafe_allow_html=True) + + st.markdown("---") + + # watch history section + st.markdown(""" +
+
+

Your watch history

+
+ """, unsafe_allow_html=True) + + history = get_user_history(user_id) + + if history: + for movie in history[:5]: + tmdb_data = search_tmdb(movie["title"]) + poster_url = get_poster_url(tmdb_data) + overview = tmdb_data.get("overview", "") + if overview and len(overview) > 120: + overview = overview[:120] + "..." + + col1, col2, col3 = st.columns([1, 7, 2]) + + with col1: + if poster_url: + st.image(poster_url, width=55) + else: + st.markdown( + "
", + unsafe_allow_html=True + ) + + with col2: + st.markdown(f"

{movie['title']}

", unsafe_allow_html=True) + if overview: + st.markdown(f"

{overview}

", unsafe_allow_html=True) + + with col3: + rating = movie.get("rating", "N/A") + color = "#4CAF50" if float(rating) >= 4 else "#FFA726" if float(rating) >= 3 else "#EF5350" + st.markdown( + f"

{rating} / 5

", + unsafe_allow_html=True + ) + st.markdown( + "

your rating

", + unsafe_allow_html=True + ) + + st.markdown("
", unsafe_allow_html=True) + else: + st.markdown("

No watch history available.

", unsafe_allow_html=True) + +else: + # welcome screen + st.markdown(""" +
+

๐Ÿฟ

+

Welcome to CinePick

+

Your personal AI-powered movie discovery engine

+
+
+

1M+

+

ratings

+
+
+
+

6,040

+

users

+
+
+
+

0.965

+

RMSE

+
+
+
+

SVD

+

model

+
+
+

โ† Enter your User ID in the sidebar to get started

+
+ """, unsafe_allow_html=True) \ No newline at end of file From d902db1f8f606aea881fd6b76c0fe013bbf2febf Mon Sep 17 00:00:00 2001 From: harmandeep2993 Date: Sun, 26 Apr 2026 04:24:03 +0200 Subject: [PATCH 3/3] chore: add python-dotenv and requests dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6b216bb..c400156 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "protobuf>=3.20.0,<4.0.0", "psutil>=7.2.2", "pytest>=9.0.3", + "python-dotenv>=1.2.2", "reload>=0.9", "requests>=2.32.5", "scikit-learn>=1.8.0",