Movie_Rec / app.py
Kshitij2604's picture
Upload app.py
96973aa verified
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
@st.cache_data
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
"""
@abstractmethod
def get_movie_details(self, movie_id: str) -> Dict[str, Any]:
"""Abstract method to fetch movie details"""
pass
@abstractmethod
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
"""
@abstractmethod
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()