0% found this document useful (0 votes)
19 views16 pages

TMDB Movie Recommendation System

This document outlines a Streamlit application for a movie recommendation system that utilizes the TMDB and Gemini APIs. It includes functions for fetching movie data, processing user preferences, and generating recommendations based on user interactions. The application features a user-friendly interface with custom styling and caching mechanisms to optimize API calls.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views16 pages

TMDB Movie Recommendation System

This document outlines a Streamlit application for a movie recommendation system that utilizes the TMDB and Gemini APIs. It includes functions for fetching movie data, processing user preferences, and generating recommendations based on user interactions. The application features a user-friendly interface with custom styling and caching mechanisms to optimize API calls.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd

import streamlit as st

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from [Link] import cosine_similarity
import numpy as np
import requests
import json
import time
from [Link] import quote

# --- 1. API Configuration & Constants ---


# ⚠️ ACTION REQUIRED: Insert your TMDB API Key here ⚠️
# I have reset this to a different placeholder. Please insert your actual TMDB key
here.
TMDB_API_KEY = "3a2fbb7f9c9ab9c091c6b434df05a4e7"

# ⚠️ ACTION REQUIRED: Insert your Gemini API Key here (or leave "" if running in an
environment that injects it) ⚠️
GEMINI_API_KEY = "AIzaSyC7zam6niVf5dCMWUmn6W88rwlwVdvAPYM"

BASE_URL = '[Link]
IMAGE_BASE_URL = '[Link]
YOUTUBE_EMBED_URL = '[Link]

# LLM Configuration (Cine Bot)


GEMINI_API_URL = "[Link]
2.5-flash-preview-09-2025:generateContent"

# Structured output schema for the Cine Bot recommendation


CINE_BOT_SCHEMA = {
"type": "OBJECT",
"properties": {
"title": {"type": "STRING", "description": "The title of the recommended
movie (must be real)."},
"reason": {"type": "STRING", "description": "A concise explanation (under
50 words) why this movie matches the user's request."},
},
"propertyOrdering": ["title", "reason"]
}

# --- 2. API Call Helper Functions ---

@st.cache_data(ttl=3600 * 24 * 7) # Cache genre map for 7 days


def fetch_tmdb_genres(api_key):
"""Fetches the TMDB genre ID to Name mapping."""
if not api_key: return {}
try:
url = f"{BASE_URL}/genre/movie/list?api_key={api_key}"
response = [Link](url, timeout=5)
response.raise_for_status()

# Returns { '28': 'Action', '12': 'Adventure', ... }


genre_map = {str(g['id']): g['name'] for g in [Link]().get('genres',
[])}
return genre_map
except [Link] as e:
# Silently fail genre fetch, as we can still display movie data (with ID
fallback)
print(f"Error fetching TMDB genres: {e}")
return {}

@st.cache_data(ttl=3600) # Cache movie data for 1 hour to reduce TMDB calls


def fetch_tmdb_movies(api_key):
"""Fetches trending movies from TMDB and enriches them with trailer keys."""
if not api_key:
[Link]("TMDB API Key is missing. Please set TMDB_API_KEY.")
return [Link]()

try:
# 1. Fetch Trending Movies (initial dataset)
url = f"{BASE_URL}/trending/movie/week?api_key={api_key}"
response = [Link](url, timeout=15)
response.raise_for_status()
data = [Link]().get('results', [])

movies = []
for item in data:
if not [Link]('poster_path') or not [Link]('overview'):
continue

# Fetch trailer key for each movie (can be slow, but cached)
trailer_key = get_movie_trailer_key(item['id'], api_key)

[Link]({
'title': [Link]('title'),
'genre': ', '.join([str(g) for g in [Link]('genre_ids', [])]), #
Store as IDs internally
'overview': [Link]('overview'),
'rating': [Link]('vote_average'),
'poster_url': f"{IMAGE_BASE_URL}{item['poster_path']}",
'id': item['id'],
'trailer_key': trailer_key,
'release_date': [Link]('release_date', 'N/A')
})

return [Link](movies)

except [Link] as e:
[Link](f"Error fetching TMDB data: {e}")
return [Link]()

@st.cache_data(ttl=3600 * 24) # Cache trailer keys for 24 hours


def get_movie_trailer_key(movie_id, api_key):
"""Fetches the main trailer key for a given movie ID."""
if not api_key: return None
try:
url = f"{BASE_URL}/movie/{movie_id}/videos?api_key={api_key}"
response = [Link](url, timeout=5)
response.raise_for_status()
videos = [Link]().get('results', [])

# Look for the official trailer


for video in videos:
if [Link]('site') == 'YouTube' and 'Trailer' in [Link]('type'):
return video['key']
return None
except [Link]:
return None

def call_gemini_api(prompt, schema=None):


"""Calls the Gemini API with structured output configuration and exponential
backoff."""
if not GEMINI_API_KEY and not st.session_state.get('is_local'):
# Allow running without key if environment injects it
pass
elif not GEMINI_API_KEY:
[Link]("Gemini API Key is missing for Cine Bot.")
return "Cine Bot is currently offline because the API key is missing."

headers = {'Content-Type': 'application/json'}


payload = {
"contents": [{"parts": [{"text": prompt}]}],
}

if schema:
payload["generationConfig"] = {
"responseMimeType": "application/json",
"responseSchema": schema
}

url = f"{GEMINI_API_URL}?key={GEMINI_API_KEY}"

for attempt in range(3):


try:
response = [Link](url, headers=headers,
data=[Link](payload), timeout=15)
response.raise_for_status()
result = [Link]()

candidate = [Link]('candidates', [{}])[0]


if candidate and [Link]('content') and
candidate['content'].get('parts'):
text_part = candidate['content']['parts'][0].get('text', '{}')

if schema:
try:
return [Link](text_part)
except [Link]:
[Link](f"Cine Bot received an unparseable JSON response:
{text_part}")
return None
else:
return text_part
return None
except [Link] as e:
if response.status_code == 429 and attempt < 2:
[Link](2 ** (attempt + 1))
continue
return f"Cine Bot API Error: HTTP {response.status_code}. Details:
{[Link]}"
except [Link] as e:
return f"Cine Bot API Request Failed: {e}"
return "Cine Bot failed after multiple retries."
# --- 3. Initial Data Loading and ML/AI Setup ---

@st.cache_resource
def load_and_process_data():
"""Load data, calculate TF-IDF, and set up state."""

# 1a. Fetch Genre Map for human-readable names


GENRE_ID_TO_NAME = fetch_tmdb_genres(TMDB_API_KEY)
GENRE_NAME_TO_ID = {v: k for k, v in GENRE_ID_TO_NAME.items()}

# 1b. Fetch real TMDB movie data


df = fetch_tmdb_movies(TMDB_API_KEY)

if [Link]:
# Fallback to a single dummy movie if API fails
df = [Link]({
'title': ['Data Load Error Movie'],
'genre': ['12, 14, 28'],
'overview': ["Could not load data from TMDB. Please check your API key
and network connection."],
'rating': [0.0],
'poster_url': ['[Link]
text=API+Error'],
'id': [0],
'trailer_key': [None],
'release_date': ['N/A']
})
st.session_state['data_loaded'] = False
else:
st.session_state['data_loaded'] = True

# 2. ML Setup (TF-IDF on overviews)


tfidf = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf.fit_transform(df['overview'].fillna(''))
indices = [Link]([Link], index=df['title']).drop_duplicates()

# 3. Create genre lists using names for UI


# Get all unique genre IDs present in the loaded movies
all_genres_ids = sorted(list(set([Link]() for genres in df['genre'] for g in
[Link](','))))

# Map IDs to names for the UI list


ALL_GENRE_NAMES = sorted([
GENRE_ID_TO_NAME.get(g, f"ID {g}")
for g in all_genres_ids if GENRE_ID_TO_NAME.get(g) # Only include genres we
have names for
])

# Return the data, ML artifacts, human-readable names, and the conversion map
return df, tfidf_matrix, indices, ALL_GENRE_NAMES, GENRE_ID_TO_NAME,
GENRE_NAME_TO_ID

# Load all required resources upfront


df, tfidf_matrix, indices, ALL_GENRE_NAMES, GENRE_ID_TO_NAME, GENRE_NAME_TO_ID =
load_and_process_data()

# --- 4. Streamlit UI/Theming and State Management ---

CUSTOM_CSS = """
<style>
/* Global Streamlit overrides for Dark Navy Theme */
.stApp {
background-color: #0b172a;
color: #ffffff;
font-family: 'Inter', sans-serif;
}
h1, h2, h3, h4, .stMarkdown {
color: #e0e0e0;
}

/* Custom Styling for Movie Cards */


.movie-card-container {
position: relative;
background-color: #1a2c42;
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: transform 0.3s, box-shadow 0.3s;
overflow: hidden; /* Important for clean card edges */
}
.movie-card-container:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(10, 10, 10, 0.6);
}

/* Trailer Overlay (The Hover Effect) */


.trailer-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95); /* Semi-transparent black */
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.4s ease, visibility 0.4s;
z-index: 100; /* Ensure it covers the content */
padding: 15px; /* Padding for the iframe inside the overlay */
}

/* Show overlay on card hover (Only works for custom HTML cards, kept for
Predictor Tab) */
.movie-card-container:hover .trailer-overlay {
opacity: 1;
visibility: visible;
}

.trailer-iframe {
width: 100%;
/* Adjusted height for a cinematic 16:9 aspect ratio */
height: 80%;
border: none;
border-radius: 8px;
}
/* Explore Grid Item Styling (Simplified for native [Link]) */
.explore-item {
width: 100%;
text-align: center;
margin-bottom: 20px;
padding: 10px;
border-radius: 12px;
background-color: #1a2c42;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.explore-item img {
border-radius: 8px;
}

.stButton>button {
background-color: #e50914;
color: white;
border-radius: 8px;
border: none;
padding: 8px 16px;
font-weight: bold;
transition: background-color 0.2s;
}
.stButton>button:hover {
background-color: #ff2d38;
}
.explanation-box {
background-color: #2a4059;
padding: 10px;
border-radius: 6px;
margin-top: 10px;
font-size: 0.9em;
border-left: 3px solid #00c6ff;
}
/* Custom tab appearance */
.stTabs [data-baseweb="tab-list"] {
gap: 24px;
border-bottom: 3px solid #2a4059;
justify-content: space-around;
}
.stTabs [data-baseweb="tab"] {
background-color: #1a2c42;
border-radius: 8px 8px 0 0;
padding: 10px 20px;
color: #fff;
font-weight: 600;
}
.stTabs [aria-selected="true"] {
background-color: #e50914;
color: white;
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.1);
border-top: 4px solid #e50914;
border-radius: 50%;
width: 25px;
height: 25px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
"""
[Link](CUSTOM_CSS, unsafe_allow_html=True)

# Initialize Session State


if 'user_profile' not in st.session_state:
# Use tfidf_matrix.shape to ensure pref_vector is correctly sized, even if
empty
pref_vector_size = tfidf_matrix.shape[1] if tfidf_matrix.shape else 0
st.session_state['user_profile'] = {
'likes': [],
'dislikes': [],
'watched': [],
'wishlist': [],
'pref_vector': [Link](pref_vector_size),
'pref_genres': {}
}
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
if 'is_local' not in st.session_state:
st.session_state['is_local'] = (GEMINI_API_KEY != "") # Simple check if running
locally or externally

# --- 5. Recommendation Logic Functions ---

def update_user_preferences(movie_title, interaction_type):


"""Updates user profile and preference vector based on likes/dislikes (online
learning style)."""

# Guard clause if data didn't load properly


if not st.session_state.get('data_loaded', True) or [Link]:
[Link]("Cannot update profile: Movie data is not fully loaded.")
return

profile = st.session_state['user_profile']

# 1. Update lists
if interaction_type == 'like' and movie_title not in profile['likes']:
profile['likes'].append(movie_title)
if movie_title in profile['dislikes']:
profile['dislikes'].remove(movie_title)
elif interaction_type == 'dislike' and movie_title not in profile['dislikes']:
profile['dislikes'].append(movie_title)
if movie_title in profile['likes']:
profile['likes'].remove(movie_title)

# 2. Update preference vector (Content-Based Update)


try:
idx = indices[movie_title]
movie_vector = tfidf_matrix[idx]
# Adjust preference vector based on interaction
weight = 0.1
if interaction_type == 'like':
profile['pref_vector'] = profile['pref_vector'] + (movie_vector *
weight)
elif interaction_type == 'dislike':
profile['pref_vector'] = profile['pref_vector'] - (movie_vector *
weight)

# 3. Update genre preferences (Using genre IDs as keys, as they are stable)
# The 'genre' column in df contains the ID strings
movie_genres = [Link][df['title'] == movie_title, 'genre'].iloc[0].split(',
')
for genre_id in movie_genres:
score_change = 0.5 if interaction_type == 'like' else -0.5
profile['pref_genres'][genre_id] = profile['pref_genres'].get(genre_id,
0) + score_change

st.session_state['user_profile'] = profile # Save updated state

except KeyError:
[Link](f"Could not find {movie_title} in data for preference update.")

def get_recommendations_df(genre_names_pref, exclude_list, num_recs=10):


"""Generates recommendations based on genre names, user profile, and TF-IDF."""

# Convert human-readable names back to TMDB IDs for internal filtering


target_genres_ids = set()
for name in genre_names_pref:
id_val = GENRE_NAME_TO_ID.get(name)
if id_val:
target_genres_ids.add(id_val)

# Guard clause if data didn't load properly


if not st.session_state.get('data_loaded', True) or [Link]:
return [Link](num_recs) # Return first few movies as fallback

target_genres = target_genres_ids

# Filter by genre ID match (at least one matching ID)


# The 'genre' column in df contains comma-separated IDs, so we check if any
selected ID is in the string.
filtered_df = df[df['genre'].apply(lambda x: any(g in x for g in
target_genres))].copy()
if filtered_df.empty:
filtered_df = [Link]() # Fallback to all movies if genre filter yields
nothing

# 1. Content-Based Similarity (Hybrid)


user_vector = st.session_state['user_profile']['pref_vector']

base_sim = [Link](len(filtered_df))
if [Link](user_vector) != 0:
user_vector_normalized = user_vector / [Link](user_vector)
filtered_indices = filtered_df.index

if not filtered_indices.empty:
filtered_tfidf = tfidf_matrix[filtered_indices]
# Calculate similarity (dot product of user vector and movie vectors)
base_sim =
filtered_tfidf.dot(user_vector_normalized.T).toarray().flatten()

filtered_df['sim_score'] = base_sim

# 2. Apply Penalties/Exclusions
filtered_df['final_score'] = filtered_df['sim_score'] + filtered_df['rating'] *
0.1

# Exclude already watched/suggested movies


exclude_titles = set(exclude_list) | set(st.session_state['user_profile']
['watched'])
filtered_df = filtered_df[~filtered_df['title'].isin(exclude_titles)]

# Sort and return


recommended_df = filtered_df.sort_values(by='final_score',
ascending=False).head(num_recs)
return recommended_df

def render_movie_card(movie, explanation=None, show_trailer_button=False):


"""Renders a single movie card with details and poster/trailer."""

# This card uses complex HTML which is suitable for the main predictor tab
[Link](f"<div class='movie-card-container'>", unsafe_allow_html=True)
col1, col2 = [Link]([1, 3])

with col1:
# Custom HTML for image with trailer hover overlay
trailer_html = ""
if movie['trailer_key']:
trailer_html = f"""
<div class='trailer-overlay'>
<iframe
class='trailer-iframe'
src='{YOUTUBE_EMBED_URL}{movie['trailer_key']}?
autoplay=1&mute=1&controls=0'
title='YouTube video player'
frameborder='0'
allow='autoplay; encrypted-media; gyroscope; picture-in-
picture'
allowfullscreen
></iframe>
</div>
"""

title_escaped = movie['title'].replace("'", "\\'")

# NOTE: This custom HTML structure works reliably in the main body
(Predictor tab)
card_image_html = f"""
<div style="position: relative;">
{trailer_html}
<img
src="{movie['poster_url']}"
alt="{title_escaped}"
style="border-radius: 8px; width: 100%; height: auto; display:
block;"
onerror="[Link]=null;
[Link]='[Link]
>
</div>
"""
[Link](card_image_html, unsafe_allow_html=True)

if show_trailer_button and movie['trailer_key']:


[Link](f"<a href='{YOUTUBE_EMBED_URL}{movie['trailer_key']}'
target='_blank' style='text-decoration: none;'><button class='stButton'>▶️ Play
Trailer (New Tab)</button></a>", unsafe_allow_html=True)
elif show_trailer_button:
[Link]("No trailer found.")

with col2:
[Link](f"### {movie['title']}")
[Link](f"**Rating:** ⭐ {movie['rating']:.1f} / 10 | **Release:**
{movie['release_date']}")

# Display genre names instead of raw IDs (requires a quick conversion)


genre_ids = movie['genre'].split(', ')
genre_names = [GENRE_ID_TO_NAME.get(g, f"ID {g}") for g in genre_ids]
[Link](f"**Genres:** {', '.join(genre_names)}")

[Link](f"**Overview:** {movie['overview']}")

if explanation:
[Link](f"<div class='explanation-box'>**AI Reason:**
{explanation}</div>", unsafe_allow_html=True)

# Add like/dislike buttons for profile learning


like_col, dislike_col = [Link](2)
with like_col:
if [Link]("👍 Like", key=f"like_{movie['id']}_{[Link]()}"):
update_user_preferences(movie['title'], 'like')
[Link](f"You liked '{movie['title']}'! Profile updated.",
icon='👍')

with dislike_col:
if [Link]("👎 Dislike",
key=f"dislike_{movie['id']}_{[Link]()}"):
update_user_preferences(movie['title'], 'dislike')
[Link](f"You disliked '{movie['title']}'. We will avoid similar
movies.", icon='👎')

[Link]("</div>", unsafe_allow_html=True)
[Link]("---")

# --- 6. Tab Implementations ---

def tab_movie_predictor():
"""Tab 1: Movie Predictor (MCQ, Cine Bot, Watch Together)"""
[Link]("🎬 Movie Predictor")
[Link]("---")

if not st.session_state.get('data_loaded', True) or [Link]:


[Link]("Cannot load features: Please check your TMDB API Key and reload
the app.")
return

# 1. Standard Recommender (MCQ-style)


[Link]("🔍 Personalized Recommendation")
with [Link]("recommender_form"):
col1, col2 = [Link](2)
with col1:
# Use ALL_GENRE_NAMES (e.g., 'Action', 'Comedy') for user selection
genre_select = [Link](
"Select Primary Genres",
options=ALL_GENRE_NAMES,
default=ALL_GENRE_NAMES[:2] if ALL_GENRE_NAMES else []
)
with col2:
watched_movies = [Link]("Watched Movies (to exclude)",
options=df['title'].tolist())

num_recs = [Link]("Number of Recommendations", min_value=3,


max_value=10, value=5)

submitted = st.form_submit_button("Generate Cine-Match")

if submitted and genre_select:


with [Link]("Analyzing preferences and searching the cinematic
universe..."):
# genre_select is now a list of Names, handled by
get_recommendations_df
recs_df = get_recommendations_df(genre_select, watched_movies,
num_recs)

if not recs_df.empty:
[Link](f"Top {len(recs_df)} Matches for Your Selection:")
selected_names_str = ', '.join(genre_select)
for _, movie in recs_df.iterrows():
explanation = f"Suggested due to your preference for the selected
genres ({selected_names_str}) and its content similarity based on past
interactions."
render_movie_card(movie, explanation, show_trailer_button=True)
else:
[Link]("No new movies match your criteria. Try adjusting your
selections!")

[Link]("---")

# 2. Cine Bot (LLM Chatbot)


[Link]("🤖 Cine Bot: Chat for a Recommendation")
[Link]("Ask the bot anything like: 'I just watched Interstellar, what
should I watch next?'")

# Display chat history


for message in st.session_state.chat_history:
st.chat_message(message["role"]).markdown(message["content"])

if prompt := st.chat_input("Ask Cine Bot..."):


st.session_state.chat_history.append({"role": "user", "content": prompt})
st.chat_message("user").markdown(prompt)

with [Link]([Link](f"<div class='spinner'></div> Thinking...",


unsafe_allow_html=True)):

# System instruction for the bot


all_movie_titles = ', '.join(df['title'].tolist())
system_instruction = (
f"You are an expert movie recommender bot. Based on the user's
query, recommend exactly ONE movie title that is likely to be a real, well-known
movie. "
"You MUST respond ONLY with a JSON object conforming to the
provided schema. "
f"For context, the available titles in the database are:
{all_movie_titles}. If possible, pick one of these."
)

# API call to Gemini


gemini_prompt = f"User query: '{prompt}'. Recommend a movie and explain
why it matches (under 50 words)."

structured_response = call_gemini_api(
prompt=gemini_prompt,
schema=CINE_BOT_SCHEMA
)

if isinstance(structured_response, str):
st.chat_message("assistant").error(structured_response)
st.session_state.chat_history.append({"role": "assistant", "content":
structured_response})
elif structured_response:
try:
title = structured_response['title']
reason = structured_response['reason']

movie_match = df[df['title'] == title]

with st.chat_message("assistant"):
if not movie_match.empty:
[Link](f"**Cine Bot says:** Here is a perfect match
for you!")
movie_data = movie_match.iloc[0].to_dict()
render_movie_card([Link](movie_data),
explanation=reason, show_trailer_button=True)
else:
[Link](f"Cine Bot recommended **{title}**, but I
couldn't find it in the trending catalog. **Reason:** {reason}")
[Link]("Try asking for a more popular movie!")

st.session_state.chat_history.append({"role": "assistant",
"content": f"**Cine Bot recommends:** {title}. **Reason:** {reason}"})

except Exception as e:
[Link](f"Error processing Cine Bot response: {e}")
st.session_state.chat_history.append({"role": "assistant",
"content": "I apologize, I ran into an error generating that recommendation. Could
you try asking again?"})
else:
st.session_state.chat_history.append({"role": "assistant", "content":
"I apologize, I could not connect to my AI brain. Please try again in a moment."})

[Link]("---")
# 3. Watch Together (Placeholder)
[Link]("👥 Watch Together: Group Recommendation (Advanced Feature)")
[Link]("This feature would blend user profiles using collaborative filtering
and content scoring. For now, it blends genre names selected by multiple users.")

with [Link]("Configure Group Preferences"):


u_genres = []
u_genres.append([Link]("User 1 Preferences (Genres)",
options=ALL_GENRE_NAMES, key='u1g', default=ALL_GENRE_NAMES[:2] if ALL_GENRE_NAMES
else []))
u_genres.append([Link]("User 2 Preferences (Genres)",
options=ALL_GENRE_NAMES, key='u2g', default=ALL_GENRE_NAMES[2:4] if
len(ALL_GENRE_NAMES) > 4 else []))

group_submitted = [Link]("Find Group Match", key='group_match')

if group_submitted:
all_genres = set()
for g_list in u_genres:
all_genres.update(g_list)

if all_genres:
with [Link]("Blending tastes for a group consensus..."):
# Pass the genre names to the function
group_recs_df = get_recommendations_df(list(all_genres), [],
num_recs=6)

if not group_recs_df.empty:
[Link](f"Group Picks:")
for _, movie in group_recs_df.iterrows():
# Display the genre name in the explanation
first_genre_id = movie['genre'].split(',')[0].strip()
first_genre_name = GENRE_ID_TO_NAME.get(first_genre_id,
f"ID {first_genre_id}")

explanation = f"Chosen because it contains a wide array of


popular genres, including {first_genre_name}."
render_movie_card(movie, explanation,
show_trailer_button=True)
else:
[Link]("Could not find a match for the combined group
preferences.")
else:
[Link]("Please select genres for the users.")

def tab_explore():
"""Tab 2: Explore (Trending/Top Picks, Like/Dislike)"""
[Link](" Explore the Catalog")
[Link]("Discover the latest trending movies from **TMDB**. Click any
poster to see its details in the Predictor tab!")

if not st.session_state.get('data_loaded', True) or [Link]:


[Link]("Cannot load data: Please check your TMDB API Key and reload the
app.")
return

[Link]("🔥 Trending This Week")


# Simulate an infinite scroll/grid layout
[Link]("<div class='movie-grid'>", unsafe_allow_html=True)

# Display all loaded movies (up to 20 from TMDB trending)


cols_per_row = 4
for i in range(0, len(df), cols_per_row):
cols = [Link](cols_per_row)
for j, movie in [Link][i:i + cols_per_row].iterrows():

with cols[j % cols_per_row]:

# --- FIX APPLIED: Using native [Link] for reliable rendering ---
[Link]("<div class='explore-item'>", unsafe_allow_html=True)

# Use [Link] for the poster display


[Link](
movie['poster_url'],
caption=None,
use_column_width='always',
output_format='PNG',
# Using a placeholder image for fall back is complicated with
[Link]
)

# Add title and rating using markdown


[Link](f"""
<div style="text-align: center; margin-top: -10px; padding-
bottom: 10px;">
<p style="margin: 0; font-weight: bold; font-size: 0.9em;
color: #fff;">{movie['title']}</p>
<p style="margin: 0; font-size: 0.8em; color: #aaa;">⭐
{movie['rating']:.1f}</p>
</div>
""", unsafe_allow_html=True)

[Link]("</div>", unsafe_allow_html=True)
# --- END FIX ---

# Interaction buttons
like_col, dislike_col = [Link](2)
with like_col:
if [Link]("👍", key=f"E_like_{movie['id']}"):
update_user_preferences(movie['title'], 'like')
[Link](f"Liked '{movie['title']}'! Profile updated.",
icon='👍')
with dislike_col:
if [Link]("👎", key=f"E_dislike_{movie['id']}"):
update_user_preferences(movie['title'], 'dislike')
[Link](f"Disliked '{movie['title']}'. Profile updated.",
icon='👎')

[Link]("</div>", unsafe_allow_html=True)

def tab_personality_profile():
"""Tab 3: Movie Personality Profile (User Data)"""
[Link]("👤 Movie Personality Profile")
[Link]("This is your personalized cinematic taste profile, constantly
updated by the AI based on your interactions.")

profile = st.session_state['user_profile']

[Link]("Taste Summary")

# Calculate top genres


if profile['pref_genres']:
# pref_genres stores ID:Score, so we sort by score
sorted_genres = sorted(profile['pref_genres'].items(), key=lambda item:
item[1], reverse=True)

# Convert ID (key) to Name for display


top_genres_names = [
# Fallback to 'ID X' if the genre name couldn't be fetched
f"{GENRE_ID_TO_NAME.get(g, f'ID {g}')} (Score: {s:.1f})"
for g, s in sorted(sorted_genres, key=lambda item: item[1],
reverse=True) if s > 0
]

if top_genres_names:
# Display genre names
[Link](f"Your cinematic personality leans towards these Genres: **{',
'.join(top_genres_names[:5])}**.")
else:
[Link]("Start interacting (Like/Dislike) to discover your
personality!")
else:
[Link]("Start interacting (Like/Dislike) to discover your personality!")

[Link]("Activity Log")

col_l, col_d = [Link](2)


with col_l:
[Link]("👍 Liked Movies")
if profile['likes']:
[Link](f"**Total:** {len(profile['likes'])}")
[Link]("- " + "\n- ".join(profile['likes']))
else:
[Link]("*No liked movies yet.*")

with col_d:
[Link]("👎 Disliked Movies")
if profile['dislikes']:
[Link](f"**Total:** {len(profile['dislikes'])}")
[Link]("- " + "\n- ".join(profile['dislikes']))
else:
[Link]("*No disliked movies yet.*")

[Link]("---")

[Link]("Edit Preferences")
if [Link]("Reset All User Interactions", help="This will clear your likes,
dislikes, and learned profile vector."):
# Re-initialize the preference vector size size correctly
pref_vector_size = tfidf_matrix.shape[1] if tfidf_matrix.shape else 0
st.session_state['user_profile'] = {
'likes': [],
'dislikes': [],
'watched': [],
'wishlist': [],
'pref_vector': [Link](pref_vector_size),
'pref_genres': {}
}
st.session_state.chat_history = []
[Link]("Your profile has been reset. Time to build a new cinematic
personality!")

# --- 7. Main Application Structure ---

def main():
st.set_page_config(
page_title="CineMate – Movie Recommendation App",
layout="wide",
initial_sidebar_state="collapsed"
)

[Link]("CineMate 🍿")
[Link]("AI-Powered Cinematic Recommendations from TMDB")

if not TMDB_API_KEY or TMDB_API_KEY == "YOUR_TMDB_API_KEY_HERE":


[Link]("🚨 **TMDB API Key Missing:** Please replace
`YOUR_TMDB_API_KEY_HERE` with your actual TMDB key to load movie data.")
[Link]()

if not st.session_state.get('data_loaded', True) or [Link]:


[Link]("⚠️ Data loading failed. Check your API key or network
connection and try reloading the app.")

# Custom tab creation to mimic bottom navigation


tabs = [Link]([" Movie Predictor", "🗺️Explore", "👤 Movie Personality
Profile"])

with tabs[0]:
tab_movie_predictor()

with tabs[1]:
tab_explore()

with tabs[2]:
tab_personality_profile()

if __name__ == "__main__":
main()

Common questions

Powered by AI

Cine Bot provides users with tailored movie recommendations via a chatbot interface. It is implemented by calling the Gemini API, which uses structured prompts and a response schema to generate recommendations. The chatbot is interactive, allowing for conversational movie suggestions based on user queries, and integrates genre and movie data to refine its offerings .

The application enhances user experience with a visually appealing UI that includes custom CSS styling for movie cards, tabs, and interaction buttons, maintaining a cohesive and engaging design. Features like hover effects and dynamic recommendations make it interactive and user-friendly. The use of intuitive layouts and easy-to-navigate tabs further refines the user experience .

Cine Bot utilizes a structured output schema that mandates the JSON object to include properties like 'title' and 'reason'. This schema ensures that recommendations are consistent and interpretable, facilitating easy integration with other parts of the application and improving the clarity of communications with the user .

The application features several error handling strategies for API requests, including try-except blocks to catch exceptions such as RequestException and HTTPError. For instance, it uses a fallback mechanism to provide default data in case of failures. Additionally, when fetching movie data or genres, the system logs errors and continues to operate in a reduced capacity, ensuring user experience is maintained as much as possible .

The system employs the TF-IDF Vectorizer to transform movie overviews into a numerical format that reflects the significance of words. This transformed data is then used to create a preference vector for users based on their interactions with movies (likes or dislikes). This vector is critical for computing content-based recommendations by comparing the user's interest profile with the movie data .

Beyond TF-IDF, the application incorporates machine learning techniques like cosine similarity to compare movie overview vectors with the user preference vector for content-based filtering. This approach allows the system to provide personalized movie suggestions by aligning user interests with similar movie data. It dynamically adjusts recommendations as user interactions alter the preference vector .

The TMDB API is primarily used to fetch trending movies and their associated metadata, such as genre IDs, overviews, ratings, poster URLs, and trailer keys. The application utilizes this API to gather movie data, cache it for efficiency, and enable features like movie recommendations and predictions based on user preferences .

Caching significantly boosts the application's efficiency and performance by storing essential data like genres and movie information. This reduces the need for repeated API calls, minimizing latency and network load. Cached data is kept up-to-date with specific time-to-live settings, ensuring data relevance and minimizing user wait time while maintaining system responsiveness .

Session state is crucial for maintaining continuity and personalization within the application. It stores user-specific information, such as preferences, movie interactions, and chat history, ensuring that these persist even when the user navigates different tabs. This management is done using Streamlit’s session state functionality, providing a fluid experience that adapts to user inputs across sessions .

User interactions such as likes and dislikes dynamically update a user's profile and preference vector. When a user likes or dislikes a movie, the system adjusts the preference vector to reflect these preferences, enhancing the accuracy of content-based recommendations by aligning future suggestions more closely with the user's confirmed tastes .

You might also like