File size: 11,143 Bytes
cc2570f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692ccba
cc2570f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692ccba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc2570f
 
 
 
 
 
 
 
 
 
 
 
692ccba
cc2570f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883f9a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import streamlit as st
import requests
from transformers import pipeline

# Enter your TMDb API key here
TMDB_API_KEY = '2cc77b08a475b510f54a0dcca2c5c0c7'
IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w200"  # TMDb base URL for poster images (width 200)

@st.cache_resource
def load_emotion_analyzer():
    return pipeline("text-classification", model="j-hartmann/emotion-english-distilroberta-base")

emotion_analyzer = load_emotion_analyzer()

def analyze_emotion(description):
    """Analyze emotion of a movie's description."""
    emotions = emotion_analyzer(description)
    return emotions[0]['label']  # Return the top emotion

def get_movie_genres():
    """Fetches movie genres from TMDb API and returns a dictionary of genres."""
    url = f'https://api.themoviedb.org/3/genre/movie/list?api_key={TMDB_API_KEY}&language=en-US'
    response = requests.get(url)
    if response.status_code == 200:
        genres = response.json().get('genres', [])
        return {genre['name']: genre['id'] for genre in genres}
    else:
        return None  # Return None if the request failed

def get_top_movies_by_genre(genre_id):
    """Fetches the top 10 highest-rated movies for a given genre ID from TMDb, sorted by title length."""
    url = f'https://api.themoviedb.org/3/discover/movie?api_key={TMDB_API_KEY}&language=en-US&sort_by=vote_average.desc&vote_count.gte=1000&with_genres={genre_id}'
    response = requests.get(url)
    if response.status_code == 200:
        movies = response.json().get('results', [])[:10]  # Get the top 10 results
        # Sort movies by title length, shortest to longest
        return sorted([(movie['title'], movie['vote_average'], movie['id'], movie['poster_path']) for movie in movies], key=lambda x: len(x[0]))
    else:
        return None

def get_similar_movies(movie_id, genre_id):
    """Fetches similar movies for a given movie ID from TMDb and filters by the selected genre."""
    url = f'https://api.themoviedb.org/3/movie/{movie_id}/similar?api_key={TMDB_API_KEY}&language=en-US'
    response = requests.get(url)
    if response.status_code == 200:
        movies = response.json().get('results', [])[:15]
        # Include the movie description (overview) in the returned values
        return [
            (movie['title'], movie['vote_average'], movie['poster_path'], movie['id'], movie['overview'])
            for movie in movies if genre_id in movie['genre_ids'] and 'overview' in movie
        ]
    else:
        return None

def get_movie_details(movie_id):
    """Fetches detailed information for a given movie ID from TMDb, including description, release date, director, and cast."""
    url = f'https://api.themoviedb.org/3/movie/{movie_id}?api_key={TMDB_API_KEY}&language=en-US&append_to_response=credits'
    response = requests.get(url)
    if response.status_code == 200:
        movie = response.json()
        directors = [member['name'] for member in movie['credits']['crew'] if member['job'] == 'Director']
        cast = [member['name'] for member in movie['credits']['cast'][:5]]  # Get top 5 cast members
        return {
            "title": movie['title'],
            "rating": movie['vote_average'],
            "description": movie['overview'],
            "poster_path": movie['poster_path'],
            "release_date": movie['release_date'],
            "director": ", ".join(directors),
            "cast": ", ".join(cast)
        }
    else:
        return None

# Initialize session state to store selected movies and recommendations
if 'selected_movies' not in st.session_state:
    st.session_state['selected_movies'] = set()
if 'recommendations' not in st.session_state:
    st.session_state['recommendations'] = []
if 'last_genre' not in st.session_state:
    st.session_state['last_genre'] = None

# Streamlit app title
st.title("Movie Recommender")

# CSS for alignment and button styling
st.markdown("""
    <style>
    .movie-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: flex-start;
    }
    .movie-caption {
        min-height: 50px;  /* Set a consistent height for caption sections */
        display: flex;
        align-items: center;
        justify-content: center;
        text-align: center;
    }
    .movie-caption button {
        width: 100%;
    }
    </style>
""", unsafe_allow_html=True)

# Fetch genres from TMDb and display in a dropdown
genres = get_movie_genres()
if genres is None:
    st.error("Failed to load movie genres. Please check your API key or connection.")
else:
    genre_name = st.selectbox("Select a movie genre:", [""] + list(genres.keys()))  # Include an empty option

    # Check if genre has changed, and reset selections if so
    if genre_name != st.session_state['last_genre']:
        st.session_state['selected_movies'].clear()
        st.session_state['recommendations'].clear()
        st.session_state['last_genre'] = genre_name

    # Only fetch and display movies if a genre is selected
    if genre_name:
        st.write("Please select at least one movie to generate recommendations.")
        genre_id = genres[genre_name]  # Get the genre ID based on the selected genre name
        top_movies = get_top_movies_by_genre(genre_id)  # Fetch top 10 movies in this genre, sorted by title length

        # Check if movies were successfully loaded
        if top_movies is None:
            st.error("Failed to load movies. Please check your API key or connection.")
        else:
            # Display the top 10 movies as clickable titles in a 5x2 grid layout with aligned rows
            st.write(f"Top 10 movies in the {genre_name} genre:")
            columns = st.columns(5)  # Create 5 columns for each row

            # First row of 5 movies
            for index, (title, _, movie_id, poster_path) in enumerate(top_movies[:5]):
                col = columns[index]
                with col:
                    st.markdown("<div class='movie-container'>", unsafe_allow_html=True)

                    # Display image
                    st.image(f"{IMAGE_BASE_URL}{poster_path}", use_container_width=True)

                    # Display clickable title as a button to toggle selection, with consistent height
                    if st.button(f"{title} {'✅' if movie_id in st.session_state['selected_movies'] else ''}", key=f"title_{movie_id}"):
                        if movie_id in st.session_state['selected_movies']:
                            st.session_state['selected_movies'].remove(movie_id)
                        else:
                            st.session_state['selected_movies'].add(movie_id)

                    st.markdown("</div>", unsafe_allow_html=True)

            # Second row of 5 movies
            for index, (title, _, movie_id, poster_path) in enumerate(top_movies[5:]):
                col = columns[index]
                with col:
                    st.markdown("<div class='movie-container'>", unsafe_allow_html=True)

                    # Display image
                    st.image(f"{IMAGE_BASE_URL}{poster_path}", use_container_width=True)

                    # Display clickable title as a button to toggle selection, with consistent height
                    if st.button(f"{title} {'✅' if movie_id in st.session_state['selected_movies'] else ''}", key=f"title_2_{movie_id}"):
                        if movie_id in st.session_state['selected_movies']:
                            st.session_state['selected_movies'].remove(movie_id)
                        else:
                            st.session_state['selected_movies'].add(movie_id)

                    st.markdown("</div>", unsafe_allow_html=True)

    # Button to generate recommendations based on selected movies
    if st.button("Show Recommendations"):
        if st.session_state['selected_movies']:
            # Step 1: Collect emotions of user-selected movies
            with st.spinner("Analyzing emotions of selected movies..."):
                selected_emotions = []
                for movie_id in st.session_state['selected_movies']:
                    movie_details = get_movie_details(movie_id)
                    if movie_details:
                        emotion = analyze_emotion(movie_details["description"])
                        selected_emotions.append(emotion)

                # Step 2: Collect similar movies for all selected movies
                all_similar_movies = []
                for movie_id in st.session_state['selected_movies']:
                    similar_movies = get_similar_movies(movie_id, genre_id)
                    if similar_movies:
                        all_similar_movies.extend(similar_movies)

                # Step 3: Filter similar movies by matching emotions and remove duplicates
                recommendations = []
                seen_movie_ids = set(st.session_state['selected_movies'])  # Start with user-selected movies to exclude them

            with st.spinner("Filtering movies by description emotion..."):
                for title, rating, poster_path, movie_id, description in all_similar_movies:
                    if movie_id not in seen_movie_ids:  # Exclude already-seen movies
                        movie_emotion = analyze_emotion(description)
                        if movie_emotion in selected_emotions:
                            seen_movie_ids.add(movie_id)  # Mark this movie as seen to prevent duplicates
                            recommendations.append((title, rating, poster_path, movie_id, movie_emotion))

            # Store unique recommendations in session state
            st.session_state['recommendations'] = recommendations

            # Step 4: Display final recommendations
            if st.session_state['recommendations']:
                st.write("Based on your selections, you might also enjoy these top movies:")
                for title, rating, poster_path, movie_id, emotion in sorted(st.session_state['recommendations'], key=lambda x: x[1], reverse=True)[:5]:
                    movie_details = get_movie_details(movie_id)
                    if movie_details:
                        col1, col2 = st.columns([1, 3])
                        with col1:
                            st.image(f"{IMAGE_BASE_URL}{movie_details['poster_path']}", use_container_width=True)
                        with col2:
                            st.markdown(f"**Title:** {movie_details['title']}")
                            st.markdown(f"**TMDb Rating:** {movie_details['rating']}")
                            st.markdown(f"**Description:** {movie_details['description']}")

                            # Additional Information button with overlay using expander
                            with st.expander("Additional Information"):
                                st.markdown(f"**Release Date:** {movie_details['release_date']}")
                                st.markdown(f"**Director:** {movie_details['director']}")
                                st.markdown(f"**Cast:** {movie_details['cast']}")
            else:
                st.warning("No movies matched your selected emotions.")