File size: 16,364 Bytes
f00d260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36b135f
f00d260
 
 
 
 
 
 
 
36b135f
 
f00d260
36b135f
 
 
 
f00d260
36b135f
 
f00d260
 
36b135f
 
 
f00d260
 
36b135f
f00d260
 
36b135f
f00d260
 
 
36b135f
f00d260
 
36b135f
f00d260
 
36b135f
 
f00d260
36b135f
 
 
 
f00d260
 
36b135f
 
 
 
f00d260
 
 
 
36b135f
 
 
f00d260
36b135f
f00d260
 
 
 
36b135f
 
 
f00d260
 
 
 
36b135f
 
 
f00d260
 
 
 
 
36b135f
f00d260
36b135f
 
f00d260
36b135f
f00d260
36b135f
 
f00d260
36b135f
f00d260
 
36b135f
 
f00d260
 
 
 
36b135f
 
 
f00d260
 
36b135f
 
 
f00d260
 
36b135f
f00d260
 
 
 
 
 
 
36b135f
 
f00d260
 
36b135f
 
 
f00d260
 
36b135f
f00d260
 
 
 
36b135f
f00d260
 
36b135f
f00d260
36b135f
f00d260
36b135f
f00d260
 
36b135f
 
 
f00d260
 
36b135f
f00d260
 
 
 
 
 
d8e6753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f00d260
459022c
f00d260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import gradio as gr
import logging
from services.tmdb_client import tmdb_client
from utils.config import config

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('movie_app.log'),
        logging.StreamHandler()
    ]
)

# Create logger for this module
logger = logging.getLogger(__name__)

def extract_country_code(country_selection):
    """Extract country code from selection like 'United States (US)' -> 'US'"""
    if not country_selection:
        return "US"  # Default fallback
    
    # Extract code from parentheses
    if "(" in country_selection and ")" in country_selection:
        return country_selection.split("(")[-1].replace(")", "").strip()
    
    # Fallback if format is unexpected
    return "US"

def get_movie_recommendations(decade, country, genre, num_movies):
    """Get movie recommendations based on user preferences."""
    logger.info(f"Recommendation request: decade={decade}, country={country}, genre={genre}, num_movies={num_movies}")
    
    if not decade or not country:
        logger.warning("Missing required parameters: decade or country")
        return "Please select decade and country."
    
    # Check if country requires genre selection
    if country in config.TOP_COUNTRIES and not genre:
        logger.warning(f"Genre required for top country {country} but not provided")
        return "Please select a genre for this country."
    
    # Convert decade string to integer (e.g., "2000s" -> 2000)
    decade_year = int(decade.replace("s", ""))
    
    # Extract country code from the selection
    country_code = extract_country_code(country)
    
    # Get recommendations (genre is optional for non-top countries)
    logger.info(f"Fetching movies for {country_code}, {decade_year}, genre: {genre}")
    movies = tmdb_client.get_random_movies(
        decade=decade_year,
        country=country_code,
        genre=genre if country in config.TOP_COUNTRIES else None,
        n=num_movies
    )
    
    # Update result message based on country type
    if country in config.TOP_COUNTRIES:
        search_criteria = f"{genre} from {country} ({decade})"
    else:
        search_criteria = f"from {country} ({decade})"
    
    if not movies:
        logger.warning(f"No movies found {search_criteria}")
        return f"No movies found {search_criteria}."
    
    logger.info(f"Successfully found {len(movies)} movies {search_criteria}")
    
    # Format results with mobile-friendly movie information
    output = f"🎬 **{len(movies)} movies {search_criteria}:**\n\n"
    
    for i, movie in enumerate(movies, 1):
        # Format title - show original title if different from English title
        title_display = movie['title']
        if movie.get('original_title') and movie['original_title'] != movie['title']:
            title_display = f"{movie['title']} ({movie['original_title']})"
        
        # Create a mobile-friendly vertical card layout
        output += f"<div style='margin-bottom: 20px; padding: 15px; border: 2px solid #e0e0e0; border-radius: 12px; background: linear-gradient(145deg, #ffffff, #f5f5f5); box-shadow: 0 6px 12px rgba(0,0,0,0.1); max-width: 100%;'>\n"
        
        # Movie title header
        output += f"<h2 style='margin: 0 0 15px 0; color: #2c3e50; font-size: 1.3em; text-align: center;'>{i}. {title_display}</h2>\n"
        
        # Poster image section - centered and smaller
        if movie.get('poster_url'):
            output += f"<div style='display: flex; justify-content: center; align-items: center; margin: 20px 0; padding: 10px;'>\n"
            output += f"<img src='{movie['poster_url']}' style='width: 130px; height: 195px; object-fit: cover; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);' alt='{movie['title']} poster'>\n"
            output += f"</div>\n"
        
        # Basic Info Row - more compact
        output += f"<div style='display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; justify-content: center;'>\n"
        output += f"<div style='background: #ecf0f1; padding: 6px 10px; border-radius: 6px; font-size: 0.9em;'><strong>Year:</strong> {movie['year']}</div>\n"
        
        if movie.get('has_omdb_data') and movie.get('runtime') != 'N/A':
            output += f"<div style='background: #ecf0f1; padding: 6px 10px; border-radius: 6px; font-size: 0.9em;'><strong>Runtime:</strong> {movie['runtime']}</div>\n"
        
        if movie.get('has_omdb_data') and movie.get('rated') != 'N/A':
            output += f"<div style='background: #ecf0f1; padding: 6px 10px; border-radius: 6px; font-size: 0.9em;'><strong>Rated:</strong> {movie['rated']}</div>\n"
        
        output += f"</div>\n"
        
        # Director and Cast (if available) - more compact
        if movie.get('has_omdb_data'):
            if movie.get('director') != 'N/A':
                output += f"<p style='margin: 6px 0; color: #34495e; font-size: 0.95em; text-align: center;'><strong>🎬 Director:</strong> {movie['director']}</p>\n"
            
            if movie.get('actors') != 'N/A':
                actors = movie['actors'][:120] + '...' if len(movie['actors']) > 120 else movie['actors']
                output += f"<p style='margin: 6px 0; color: #34495e; font-size: 0.95em; text-align: center;'><strong>🎭 Cast:</strong> {actors}</p>\n"
        
        # Ratings Section - 2x2 grid layout for mobile
        output += f"<div style='background: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0;'>\n"
        output += f"<h4 style='margin: 0 0 10px 0; color: #2c3e50; text-align: center;'>πŸ“Š Ratings</h4>\n"
        output += f"<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 8px; max-width: 300px; margin: 0 auto;'>\n"
        
        # TMDB Rating
        output += f"<div style='background: #3498db; color: white; padding: 8px; border-radius: 6px; text-align: center;'>\n"
        output += f"<div style='font-weight: bold; font-size: 0.9em;'>TMDB</div>\n"
        output += f"<div style='font-size: 0.9em;'>⭐ {movie['rating']}/10</div>\n"
        output += f"<div style='font-size: 0.7em;'>({movie['vote_count']} votes)</div>\n"
        output += f"</div>\n"
        
        # IMDB Rating (if available)
        if movie.get('has_omdb_data') and movie.get('imdb_rating') != 'N/A':
            output += f"<div style='background: #f39c12; color: white; padding: 8px; border-radius: 6px; text-align: center;'>\n"
            output += f"<div style='font-weight: bold; font-size: 0.9em;'>IMDb</div>\n"
            output += f"<div style='font-size: 0.9em;'>⭐ {movie['imdb_rating']}/10</div>\n"
            if movie.get('imdb_votes') != 'N/A':
                output += f"<div style='font-size: 0.7em;'>({movie['imdb_votes']} votes)</div>\n"
            output += f"</div>\n"
        
        # Rotten Tomatoes (if available)
        if movie.get('has_omdb_data') and movie.get('rotten_tomatoes') != 'N/A':
            output += f"<div style='background: #e74c3c; color: white; padding: 8px; border-radius: 6px; text-align: center;'>\n"
            output += f"<div style='font-weight: bold; font-size: 0.9em;'>RT</div>\n"
            output += f"<div style='font-size: 0.9em;'>πŸ… {movie.get('rotten_tomatoes')}</div>\n"
            output += f"</div>\n"
        
        # Metacritic (if available)
        if movie.get('has_omdb_data') and movie.get('metascore') != 'N/A':
            output += f"<div style='background: #9b59b6; color: white; padding: 8px; border-radius: 6px; text-align: center;'>\n"
            output += f"<div style='font-weight: bold; font-size: 0.9em;'>Meta</div>\n"
            output += f"<div style='font-size: 0.9em;'>πŸ“Š {movie['metascore']}/100</div>\n"
            output += f"</div>\n"
        
        output += f"</div>\n"
        output += f"</div>\n"
        
        # Plot/Overview - more compact
        plot_text = movie.get('plot', movie['overview']) if movie.get('has_omdb_data') else movie['overview']
        plot_display = plot_text[:250] + '...' if len(plot_text) > 250 else plot_text
        output += f"<p style='margin: 12px 0; color: #555; line-height: 1.4; background: #fff; padding: 10px; border-radius: 6px; border-left: 3px solid #3498db; font-size: 0.95em;'><strong>πŸ“ Plot:</strong> {plot_display}</p>\n"
        
        # Awards (if available) - more compact
        if movie.get('has_omdb_data') and movie.get('awards') != 'N/A' and movie['awards'] != 'No awards.':
            awards_text = movie['awards'][:150] + '...' if len(movie['awards']) > 150 else movie['awards']
            output += f"<p style='margin: 8px 0; color: #e67e22; background: #fef9e7; padding: 8px; border-radius: 6px; border-left: 3px solid #f39c12; font-size: 0.9em;'><strong>πŸ† Awards:</strong> {awards_text}</p>\n"
        
        # Streaming Services Section - more compact
        streaming_services = movie.get('all_services', [])
        if streaming_services:
            output += f"<div style='background: #e8f5e8; padding: 12px; border-radius: 8px; margin: 12px 0; border: 2px solid #27ae60;'>\n"
            output += f"<h4 style='margin: 0 0 10px 0; color: #27ae60; display: flex; align-items: center; justify-content: center; font-size: 1em;'><span style='margin-right: 6px;'>πŸ“Ί</span>Watch Now</h4>\n"
            
            # Free services
            free_services = movie.get('free_services', [])
            if free_services:
                output += f"<div style='margin-bottom: 10px;'>\n"
                output += f"<div style='font-weight: bold; color: #2c5530; margin-bottom: 6px; font-size: 0.9em; text-align: center;'>πŸ†“ Free</div>\n"
                output += f"<div style='display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;'>\n"
                for service in free_services:
                    if service.get('logo_url'):
                        output += f"<div style='display: flex; align-items: center; background: white; padding: 4px 8px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #ddd;'>\n"
                        output += f"<img src='{service['logo_url']}' style='width: 20px; height: 20px; margin-right: 4px; border-radius: 3px;' alt='{service['name']} logo'>\n"
                        output += f"<span style='font-size: 0.8em; font-weight: 500; color: #2c3e50;'>{service['name']}</span>\n"
                        output += f"</div>\n"
                    else:
                        output += f"<div style='background: white; padding: 4px 8px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #ddd; font-size: 0.8em; font-weight: 500; color: #2c3e50;'>{service['name']}</div>\n"
                output += f"</div>\n"
                output += f"</div>\n"
            
            # Subscription services
            subscription_services = movie.get('subscription_services', [])
            if subscription_services:
                output += f"<div>\n"
                output += f"<div style='font-weight: bold; color: #2c5530; margin-bottom: 6px; font-size: 0.9em; text-align: center;'>πŸ’³ Free with Subscription</div>\n"
                output += f"<div style='display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;'>\n"
                for service in subscription_services:
                    if service.get('logo_url'):
                        output += f"<div style='display: flex; align-items: center; background: white; padding: 4px 8px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #ddd;'>\n"
                        output += f"<img src='{service['logo_url']}' style='width: 20px; height: 20px; margin-right: 4px; border-radius: 3px;' alt='{service['name']} logo'>\n"
                        output += f"<span style='font-size: 0.8em; font-weight: 500; color: #2c3e50;'>{service['name']}</span>\n"
                        output += f"</div>\n"
                    else:
                        output += f"<div style='background: white; padding: 4px 8px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #ddd; font-size: 0.8em; font-weight: 500; color: #2c3e50;'>{service['name']}</div>\n"
                output += f"</div>\n"
                output += f"</div>\n"
            
            # JustWatch attribution (required by TMDB terms)
            output += f"<div style='margin-top: 8px; font-size: 0.7em; color: #666; text-align: center;'>Streaming data provided by <strong>JustWatch</strong></div>\n"
            output += f"</div>\n"
        
        # Box Office (if available) - more compact
        if movie.get('has_omdb_data') and movie.get('box_office') != 'N/A':
            output += f"<p style='margin: 8px 0; color: #27ae60; background: #eafaf1; padding: 8px; border-radius: 6px; font-size: 0.9em; text-align: center;'><strong>πŸ’° Box Office:</strong> {movie['box_office']}</p>\n"
        
        # External Links - centered and more compact
        links_html = ""
        if movie.get('imdb_url'):
            links_html += f"<div style='text-align: center; margin: 12px 0;'>\n"
            links_html += f"<a href='{movie['imdb_url']}' target='_blank' style='display: inline-block; background: linear-gradient(45deg, #f5c518, #f39c12); color: #000; padding: 8px 16px; text-decoration: none; border-radius: 6px; font-weight: bold; font-size: 0.9em; box-shadow: 0 3px 6px rgba(0,0,0,0.2); transition: transform 0.2s;' onmouseover='this.style.transform=\"translateY(-1px)\"' onmouseout='this.style.transform=\"translateY(0)\"'>🎬 View on IMDb</a>\n"
            links_html += f"</div>\n"
        
        if links_html:
            output += links_html

        output += f"</div>\n\n"
    
    return output

# Create simple Gradio interface
with gr.Blocks(
    title="Movie Recommendation", 
    theme="gstaff/xkcd",
    css="""
        footer {
            display: none !important;
        }
        .gradio-container .footer {
            display: none !important;
        }
        .gradio-container .footer-text {
            display: none !important;
        }
        .gradio-container .built-with {
            display: none !important;
        }
    """
) as app:
    gr.Markdown("# 🎬 Random Movie Recommendations")
    gr.Markdown("Select what you are in the mood for and get a movie recommendation!")
    
    with gr.Row():
        with gr.Column():
            decade = gr.Dropdown(
                choices=config.DECADES,
                label="Decade",
                value="2000s"
            )
            
            country = gr.Dropdown(
                choices=config.COUNTRIES,
                label="Country",
                value="United States of America (US)",
                filterable=True
            )
            
            genre = gr.Dropdown(
                choices=config.GENRES,
                label="Genre",
                value="Action",
                visible=True  # Initially visible since default is US
            )
            
            num_movies = gr.Slider(
                minimum=1,
                maximum=10,
                value=5,
                step=1,
                label="Number of Movies"
            )
            
            get_btn = gr.Button("Get Recommendations", variant="primary")
    
    results = gr.Markdown(label="Recommendations")
    
    # Function to update genre visibility based on country selection
    def update_genre_visibility(selected_country):
        if selected_country in config.TOP_COUNTRIES:
            return gr.update(visible=True)
        else:
            return gr.update(visible=False, value=None)
    
    # Update genre dropdown when country changes
    country.change(
        update_genre_visibility,
        inputs=[country],
        outputs=[genre]
    )
    
    # Connect the recommendation function
    get_btn.click(
        get_movie_recommendations,
        inputs=[decade, country, genre, num_movies],
        outputs=results
    )

if __name__ == "__main__":
    # Validate configuration before starting the app
    try:
        config.validate()
        logger.info("Starting Movie Recommendation App")
        app.launch()
    except Exception as e:
        logger.error(f"Failed to start application: {e}")
        raise