Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pickle | |
| import pandas as pd | |
| import numpy as np | |
| import requests | |
| from collections import deque | |
| import time | |
| import os | |
| from pathlib import Path | |
| from abc import ABC, abstractmethod | |
| from typing import List, Dict, Any, Optional, Tuple | |
| # Set page configuration | |
| st.set_page_config( | |
| page_title="Movie Recommender System", | |
| page_icon="🎬", | |
| layout="wide", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # Apply custom CSS | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 36px; | |
| font-weight: bold; | |
| color: #FF4B4B; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| padding: 20px; | |
| background-color: #1E1E1E; | |
| border-radius: 10px; | |
| } | |
| .sub-header { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #4B4BFF; | |
| margin-top: 30px; | |
| margin-bottom: 10px; | |
| } | |
| .movie-card { | |
| background-color: #2E2E2E; | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); | |
| } | |
| .rating-badge { | |
| background-color: #FFD700; | |
| color: #000; | |
| padding: 5px 10px; | |
| border-radius: 15px; | |
| font-weight: bold; | |
| display: inline-block; | |
| margin-top: 5px; | |
| } | |
| .movie-title { | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| color: white; | |
| } | |
| .movie-info { | |
| font-size: 14px; | |
| margin-bottom: 5px; | |
| color: #CCC; | |
| } | |
| .nav-button { | |
| background-color: #4B4BFF; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| margin: 5px; | |
| transition: background-color 0.3s; | |
| } | |
| .nav-button:hover { | |
| background-color: #3A3AFF; | |
| } | |
| .nav-container { | |
| display: flex; | |
| justify-content: center; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .stApp { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| @media (max-width: 768px) { | |
| .main-header { | |
| font-size: 28px; | |
| padding: 15px; | |
| } | |
| .sub-header { | |
| font-size: 20px; | |
| } | |
| .movie-card { | |
| padding: 10px; | |
| } | |
| .nav-button { | |
| padding: 8px 16px; | |
| font-size: 14px; | |
| } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Get the directory where the script is located | |
| SCRIPT_DIR = Path(__file__).parent.absolute() | |
| # Cache data loading function - separate from class to avoid caching issues | |
| def load_movie_data(script_dir: Path) -> Tuple[pd.DataFrame, np.ndarray]: | |
| """Load movie data from pickle files""" | |
| try: | |
| # Construct paths dynamically to work in different environments | |
| movie_dict_path = os.path.join(script_dir, 'movie_dict.pkl') | |
| similarity_path = os.path.join(script_dir, 'similarity.pkl') | |
| # Check if files exist | |
| if not os.path.exists(movie_dict_path): | |
| st.error(f"File not found: {movie_dict_path}") | |
| st.stop() | |
| if not os.path.exists(similarity_path): | |
| st.error(f"File not found: {similarity_path}") | |
| st.stop() | |
| # Load the data | |
| with open(movie_dict_path, 'rb') as f: | |
| movies_dict = pickle.load(f) | |
| movies_df = pd.DataFrame(movies_dict) | |
| with open(similarity_path, 'rb') as f: | |
| similarity_matrix = pickle.load(f) | |
| return movies_df, similarity_matrix | |
| except Exception as e: | |
| st.error(f"Error loading data: {e}") | |
| st.stop() | |
| # Define a Node for the Linked List | |
| class Node: | |
| """ | |
| Node class for the Linked List data structure | |
| Used for storing search history | |
| """ | |
| def __init__(self, data=None): | |
| self.data = data | |
| self.next = None | |
| # Implement Linked List as a data structure | |
| class LinkedList: | |
| """ | |
| LinkedList implementation for managing search history | |
| Demonstrates the use of a LinkedList data structure | |
| """ | |
| def __init__(self): | |
| self.head = None | |
| def append(self, data): | |
| """Append a new node to the end of the linked list""" | |
| new_node = Node(data) | |
| if self.head is None: | |
| self.head = new_node | |
| else: | |
| current = self.head | |
| while current.next: | |
| current = current.next | |
| current.next = new_node | |
| def get_all(self): | |
| """Retrieve all items in the linked list""" | |
| history = [] | |
| current = self.head | |
| while current: | |
| history.append(current.data) | |
| current = current.next | |
| return history | |
| # Abstract base class for movie providers | |
| class MovieDataProvider(ABC): | |
| """ | |
| Abstract base class for movie data providers | |
| Demonstrates abstraction and the concept of interfaces | |
| """ | |
| def get_movie_details(self, movie_id: str) -> Dict[str, Any]: | |
| """Abstract method to fetch movie details""" | |
| pass | |
| def load_data(self) -> Tuple[pd.DataFrame, np.ndarray]: | |
| """Abstract method to load movie data""" | |
| pass | |
| # Concrete implementation of MovieDataProvider using TMDB API | |
| class TMDBMovieProvider(MovieDataProvider): | |
| """ | |
| Concrete implementation of MovieDataProvider | |
| Uses TMDB API to fetch movie details | |
| Demonstrates inheritance and polymorphism | |
| """ | |
| def __init__(self, script_dir: Path): | |
| self.script_dir = script_dir | |
| self.api_key = os.environ.get('TMDB_API_KEY', 'b75fe8f52c05acaed8865a54505ed806') | |
| def get_movie_details(self, movie_id: str) -> Dict[str, Any]: | |
| """Fetch movie details from TMDB API""" | |
| try: | |
| response = requests.get( | |
| f'https://api.themoviedb.org/3/movie/{movie_id}?api_key={self.api_key}&language=en-US') | |
| data = response.json() | |
| poster_path = data.get('poster_path', '') | |
| poster_url = "https://image.tmdb.org/t/p/w500/" + poster_path if poster_path else "https://via.placeholder.com/500x750?text=No+Image+Available" | |
| return { | |
| 'poster_url': poster_url, | |
| 'overview': data.get('overview', 'No overview available'), | |
| 'release_date': data.get('release_date', 'Unknown'), | |
| 'vote_average': data.get('vote_average', 0), | |
| 'genres': [genre['name'] for genre in data.get('genres', [])] | |
| } | |
| except Exception as e: | |
| st.error(f"Error fetching movie details: {e}") | |
| return { | |
| 'poster_url': "https://via.placeholder.com/500x750?text=Error+Loading+Image", | |
| 'overview': 'Error loading movie details', | |
| 'release_date': 'Unknown', | |
| 'vote_average': 0, | |
| 'genres': [] | |
| } | |
| def load_data(self) -> Tuple[pd.DataFrame, np.ndarray]: | |
| """Load movie data from pickle files using cached function""" | |
| return load_movie_data(self.script_dir) | |
| # Recommendation strategy interface | |
| class RecommendationStrategy(ABC): | |
| """ | |
| Abstract strategy for recommendations | |
| Demonstrates the Strategy pattern | |
| """ | |
| def recommend(self, movie: str, movies_df: pd.DataFrame, similarity_matrix: np.ndarray, | |
| wishlist: deque, num_recommendations: int) -> List[Dict[str, Any]]: | |
| """Abstract method to generate recommendations""" | |
| pass | |
| # Concrete implementation of recommendation strategy for hybrid recommendations | |
| class HybridRecommendationStrategy(RecommendationStrategy): | |
| """ | |
| Concrete implementation of the recommendation strategy | |
| Implements hybrid content-based and collaborative filtering | |
| Demonstrates the Strategy pattern | |
| """ | |
| def __init__(self, movie_provider: MovieDataProvider): | |
| self.movie_provider = movie_provider | |
| def recommend(self, movie: str, movies_df: pd.DataFrame, similarity_matrix: np.ndarray, | |
| wishlist: deque, num_recommendations: int = 6) -> List[Dict[str, Any]]: | |
| """ | |
| Generate movie recommendations using a hybrid approach | |
| """ | |
| try: | |
| # Get movie index | |
| movie_index = movies_df[movies_df['title'] == movie].index[0] | |
| # Get content-based similarity scores | |
| content_distances = similarity_matrix[movie_index] | |
| # Get collaborative filtering component (based on user preferences in wishlist if available) | |
| if len(wishlist) > 0: | |
| # Find similar movies to wishlist items | |
| wishlist_indices = [movies_df[movies_df['title'] == wish_movie].index[0] | |
| for wish_movie in wishlist | |
| if wish_movie in movies_df['title'].values] | |
| if wishlist_indices: | |
| # Calculate average similarity to wishlist items | |
| wishlist_similarity = np.mean([similarity_matrix[idx] for idx in wishlist_indices], axis=0) | |
| # Combine content-based and collaborative filtering (weighted average) | |
| combined_distances = 0.7 * content_distances + 0.3 * wishlist_similarity | |
| else: | |
| combined_distances = content_distances | |
| else: | |
| combined_distances = content_distances | |
| # Get movie recommendations | |
| movie_indices = sorted(list(enumerate(combined_distances)), | |
| reverse=True, | |
| key=lambda x: x[1])[1:num_recommendations+1] | |
| recommended_movies = [] | |
| for i in movie_indices: | |
| movie_id = movies_df.iloc[i[0]].movie_id | |
| movie_title = movies_df.iloc[i[0]].title | |
| movie_details = self.movie_provider.get_movie_details(movie_id) | |
| recommended_movies.append({ | |
| 'title': movie_title, | |
| 'id': movie_id, | |
| 'poster': movie_details['poster_url'], | |
| 'overview': movie_details['overview'], | |
| 'release_date': movie_details['release_date'], | |
| 'rating': movie_details['vote_average'], | |
| 'genres': movie_details['genres'], | |
| 'similarity_score': round(i[1] * 100, 1) | |
| }) | |
| return recommended_movies | |
| except Exception as e: | |
| st.error(f"Error in recommendation algorithm: {e}") | |
| return [] | |
| # Movie Recommender System class | |
| class MovieRecommenderSystem: | |
| """ | |
| Main class for the Movie Recommender System | |
| Demonstrates encapsulation and composition | |
| """ | |
| def __init__(self): | |
| self.script_dir = Path(__file__).parent.absolute() | |
| self.movie_provider = TMDBMovieProvider(self.script_dir) | |
| self.recommendation_strategy = HybridRecommendationStrategy(self.movie_provider) | |
| # Initialize session state | |
| if 'wishlist' not in st.session_state: | |
| st.session_state.wishlist = deque(maxlen=10) # Limit to 10 movies | |
| if 'search_history' not in st.session_state: | |
| st.session_state.search_history = LinkedList() | |
| if 'show_recommendations' not in st.session_state: | |
| st.session_state.show_recommendations = False | |
| if 'current_recommendations' not in st.session_state: | |
| st.session_state.current_recommendations = [] | |
| if 'tab' not in st.session_state: | |
| st.session_state.tab = "recommend" | |
| # Load data | |
| try: | |
| self.movies, self.similarity = self.movie_provider.load_data() | |
| except Exception as e: | |
| st.error(f"Error loading data: {e}") | |
| st.stop() | |
| def add_to_wishlist(self, movie_title: str) -> None: | |
| """Add a movie to wishlist""" | |
| if movie_title not in st.session_state.wishlist: | |
| st.session_state.wishlist.append(movie_title) | |
| st.success(f'Added "{movie_title}" to your wishlist!') | |
| else: | |
| st.info(f'"{movie_title}" is already in your wishlist!') | |
| def remove_from_wishlist(self, movie_title: str) -> None: | |
| """Remove a movie from wishlist""" | |
| st.session_state.wishlist.remove(movie_title) | |
| def add_to_history(self, movie_title: str) -> None: | |
| """Add a movie to search history""" | |
| st.session_state.search_history.append(movie_title) | |
| def get_recommendations(self, movie_title: str, num_recommendations: int = 6) -> List[Dict[str, Any]]: | |
| """Get movie recommendations""" | |
| return self.recommendation_strategy.recommend( | |
| movie_title, | |
| self.movies, | |
| self.similarity, | |
| st.session_state.wishlist, | |
| num_recommendations | |
| ) | |
| def get_movie_details(self, movie_title: str) -> Dict[str, Any]: | |
| """Get details for a specific movie""" | |
| try: | |
| movie_idx = self.movies[self.movies['title'] == movie_title].index[0] | |
| movie_id = self.movies.iloc[movie_idx].movie_id | |
| return self.movie_provider.get_movie_details(movie_id) | |
| except Exception as e: | |
| st.error(f"Error getting movie details: {e}") | |
| return { | |
| 'poster_url': "https://via.placeholder.com/500x750?text=Error+Loading+Image", | |
| 'overview': 'Error loading movie details', | |
| 'release_date': 'Unknown', | |
| 'vote_average': 0, | |
| 'genres': [] | |
| } | |
| def display_movie_details(self, movie_title: str) -> None: | |
| """Display details for a movie""" | |
| movie_details = self.get_movie_details(movie_title) | |
| col1, col2 = st.columns([1, 3]) | |
| with col1: | |
| st.image(movie_details['poster_url'], width=200) | |
| with col2: | |
| st.markdown(f"### {movie_title}") | |
| st.markdown(f"**Released:** {movie_details['release_date']}") | |
| st.markdown(f"**Rating:** {movie_details['vote_average']}/10") | |
| st.markdown(f"**Genres:** {', '.join(movie_details['genres'])}") | |
| st.markdown(f"**Overview:** {movie_details['overview']}") | |
| # Add to wishlist button | |
| if st.button('Add to Wishlist', key='add_wishlist'): | |
| self.add_to_wishlist(movie_title) | |
| def display_recommendations(self) -> None: | |
| """Display movie recommendations""" | |
| if not st.session_state.current_recommendations: | |
| st.warning("No recommendations found. Please try another movie.") | |
| else: | |
| # Display recommendations in a grid | |
| cols = st.columns(3) # 3 movies per row | |
| for i, movie in enumerate(st.session_state.current_recommendations): | |
| with cols[i % 3]: | |
| st.markdown(f""" | |
| <div class="movie-card"> | |
| <div class="movie-title">{movie['title']}</div> | |
| <div class="rating-badge">⭐ {movie['rating']}/10</div> | |
| <div class="movie-info">Similarity: {movie['similarity_score']}%</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.image(movie['poster'], width=200) | |
| with st.expander("Details"): | |
| st.write(f"**Release Date:** {movie['release_date']}") | |
| st.write(f"**Genres:** {', '.join(movie['genres'])}") | |
| st.write(f"**Overview:** {movie['overview']}") | |
| if st.button('Add to Wishlist', key=f'add_wish_{i}'): | |
| self.add_to_wishlist(movie['title']) | |
| def display_wishlist(self) -> None: | |
| """Display the wishlist""" | |
| if len(st.session_state.wishlist) > 0: | |
| # Display the wishlist with additional options | |
| for i, movie in enumerate(list(st.session_state.wishlist)): | |
| col1, col2, col3 = st.columns([1, 3, 1]) | |
| with col1: | |
| try: | |
| movie_details = self.get_movie_details(movie) | |
| st.image(movie_details['poster_url'], width=150) | |
| except: | |
| st.image("https://via.placeholder.com/150x225?text=No+Image", width=150) | |
| with col2: | |
| st.markdown(f"### {movie}") | |
| try: | |
| movie_details = self.get_movie_details(movie) | |
| st.markdown(f"**Released:** {movie_details['release_date']}") | |
| st.markdown(f"**Rating:** {movie_details['vote_average']}/10") | |
| st.markdown(f"**Genres:** {', '.join(movie_details['genres'])}") | |
| with st.expander("Overview"): | |
| st.write(movie_details['overview']) | |
| except: | |
| st.write("Details not available") | |
| with col3: | |
| if st.button("Remove", key=f"remove_{i}"): | |
| self.remove_from_wishlist(movie) | |
| st.experimental_rerun() | |
| if st.button("Find Similar", key=f"similar_{i}"): | |
| st.session_state.tab = "recommend" | |
| with st.spinner('Finding similar movies...'): | |
| st.session_state.current_recommendations = self.get_recommendations(movie) | |
| st.session_state.show_recommendations = True | |
| st.experimental_rerun() | |
| st.markdown("---") | |
| # Clear wishlist button | |
| if st.button("Clear Wishlist"): | |
| st.session_state.wishlist.clear() | |
| st.success("Wishlist cleared!") | |
| st.experimental_rerun() | |
| else: | |
| st.info("Your wishlist is empty. Add movies to your wishlist by clicking 'Add to Wishlist' on movie cards.") | |
| def display_history(self) -> None: | |
| """Display the search history""" | |
| search_history_list = st.session_state.search_history.get_all() | |
| if search_history_list: | |
| # Display search history | |
| for i, movie in enumerate(search_history_list): | |
| col1, col2 = st.columns([4, 1]) | |
| with col1: | |
| st.markdown(f"### {i+1}. {movie}") | |
| with col2: | |
| if st.button("Find Again", key=f"find_again_{i}"): | |
| st.session_state.tab = "recommend" | |
| with st.spinner('Getting recommendations...'): | |
| st.session_state.current_recommendations = self.get_recommendations(movie) | |
| st.session_state.show_recommendations = True | |
| st.experimental_rerun() | |
| st.markdown("---") | |
| else: | |
| st.info("No search history available. Start searching for movie recommendations to build your history.") | |
| def run(self) -> None: | |
| """Run the movie recommender system UI""" | |
| # Main content | |
| st.markdown('<h1 class="main-header">🎬 Movie Recommender System</h1>', unsafe_allow_html=True) | |
| # Navigation buttons | |
| st.markdown('<div class="nav-container">', unsafe_allow_html=True) | |
| if st.button("🎯 Recommendations", key="nav_recommend", type="primary" if st.session_state.tab == "recommend" else "secondary"): | |
| st.session_state.tab = "recommend" | |
| st.experimental_rerun() | |
| if st.button("📋 Wishlist", key="nav_wishlist", type="primary" if st.session_state.tab == "wishlist" else "secondary"): | |
| st.session_state.tab = "wishlist" | |
| st.experimental_rerun() | |
| if st.button("📜 History", key="nav_history", type="primary" if st.session_state.tab == "history" else "secondary"): | |
| st.session_state.tab = "history" | |
| st.experimental_rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # About section | |
| with st.expander("ℹ️ About this App"): | |
| st.info(""" | |
| This movie recommendation system uses a hybrid approach combining: | |
| - Content-based filtering (based on movie features) | |
| - Collaborative filtering (based on your preferences) | |
| The system provides personalized recommendations by analyzing: | |
| - Movie genres, keywords, cast, and crew | |
| - Your wishlist preferences | |
| - Similarity scores between movies | |
| """) | |
| # Recommendations Tab | |
| if st.session_state.tab == "recommend": | |
| st.markdown('<h2 class="sub-header">Find Your Next Favorite Movie</h2>', unsafe_allow_html=True) | |
| # Movie selection with autocomplete | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| selected_movie_name = st.selectbox( | |
| 'Select a movie you like:', | |
| self.movies['title'].values | |
| ) | |
| with col2: | |
| recommendation_button = st.button('Get Recommendations', type="primary") | |
| # Display movie details for selected movie | |
| if selected_movie_name: | |
| self.display_movie_details(selected_movie_name) | |
| # Get and display recommendations | |
| if recommendation_button: | |
| with st.spinner('Finding the best movies for you...'): | |
| # Simulate processing time for better UX | |
| time.sleep(0.5) # Reduced time for better performance on Hugging Face | |
| # Add the movie to search history linked list | |
| self.add_to_history(selected_movie_name) | |
| # Get recommendations | |
| st.session_state.current_recommendations = self.get_recommendations(selected_movie_name) | |
| st.session_state.show_recommendations = True | |
| # Display recommendations | |
| if st.session_state.show_recommendations: | |
| st.markdown('<h2 class="sub-header">Recommended Movies</h2>', unsafe_allow_html=True) | |
| self.display_recommendations() | |
| # Wishlist Tab | |
| elif st.session_state.tab == "wishlist": | |
| st.markdown('<h2 class="sub-header">Your Wishlist</h2>', unsafe_allow_html=True) | |
| self.display_wishlist() | |
| # History Tab | |
| else: | |
| st.markdown('<h2 class="sub-header">Your Search History</h2>', unsafe_allow_html=True) | |
| self.display_history() | |
| # Instantiate and run the movie recommender system | |
| if __name__ == "__main__": | |
| movie_recommender = MovieRecommenderSystem() | |
| movie_recommender.run() | |