random-movie-hf / app.py
gytcrt
update text
459022c
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