StreamlitApp / src /streamlit_app.py
lenawilli's picture
Update src/streamlit_app.py
ec4448e verified
Raw
History Blame
17.6 kB
import streamlit as st
import pandas as pd
import random
import os
import json
from datetime import datetime
import requests
import difflib
st.set_page_config(layout="wide")
MOVIES_PATH = os.path.join(os.path.dirname(__file__), "movies.csv")
RATINGS_JSON_PATH = "/tmp/ratings.json"
POSTER_PLACEHOLDER = "https://via.placeholder.com/300x450.png?text=No+Poster"
TMDB_API_KEY = "d15fc170483ad01d6b3d59561432fefc"
@st.cache_data(show_spinner=False)
def get_tmdb_data(title, year=None):
url = "https://api.themoviedb.org/3/search/movie"
params = {
"api_key": TMDB_API_KEY,
"query": title,
}
if year and year != 0:
params["year"] = year
try:
response = requests.get(url, params=params)
if response.status_code == 200 and response.json()["results"]:
result = response.json()["results"][0]
poster_path = result.get("poster_path")
movie_id = result.get("id")
poster_url = f"https://image.tmdb.org/t/p/w500{poster_path}" if poster_path else POSTER_PLACEHOLDER
tmdb_link = f"https://www.themoviedb.org/movie/{movie_id}" if movie_id else None
return poster_url, tmdb_link
except Exception:
pass
return POSTER_PLACEHOLDER, None
def load_ratings():
if os.path.exists(RATINGS_JSON_PATH):
with open(RATINGS_JSON_PATH, "r") as f:
return json.load(f)
return []
def save_rating_to_json(entry):
all_ratings = load_ratings()
all_ratings = [r for r in all_ratings if r["movie_id"] != entry["movie_id"]]
all_ratings.append(entry)
with open(RATINGS_JSON_PATH, "w") as f:
json.dump(all_ratings, f, indent=2, default=str)
@st.cache_data(show_spinner=False)
def load_movies():
df = pd.read_csv(MOVIES_PATH)
df["year"] = df["title"].str.extract(r'\((\d{4})\)').fillna("0").astype(int)
df["clean_title"] = df["title"].str.replace(r'\(\d{4}\)', '', regex=True).str.strip()
df["genres"] = df["genres"].fillna("Unknown")
return df
movie_df = load_movies()
movie_titles = movie_df["title"].unique().tolist()
movie_id_to_title = dict(zip(movie_df["movieId"], movie_df["title"]))
title_to_movie_id = dict(zip(movie_df["title"], movie_df["movieId"]))
if "rated" not in st.session_state:
st.session_state.rated = []
if "quiz_history" not in st.session_state:
st.session_state.quiz_history = []
st.markdown("""
<style>
html, body, [class*="css"] {
background-color: #0b0b0b !important;
color: #f0f0f0 !important;
font-family: 'Georgia', serif;
}
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #1a1a1a;
padding: 1rem;
width: 100%;
border-bottom: 2px solid #5c1a1b;
}
.nav-left, .nav-right {
display: flex;
align-items: center;
}
.nav-item {
color: #f0f0f0;
font-size: 18px;
text-decoration: none;
margin-right: 1rem;
transition: color 0.3s;
}
.nav-item:hover {
color: #b32d2e;
}
.search-form {
display: flex;
width: 100%;
max-width: 700px;
}
.search-input {
flex: 1;
padding: 0.5rem;
font-size: 16px;
border: none;
border-radius: 4px 0 0 4px;
background-color: #333;
color: white;
}
.search-button {
padding: 0.5rem 1rem;
font-size: 16px;
background-color: #5c1a1b;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.search-button:hover {
background-color: #732323;
}
.star {
color: #d4af37;
font-size: 1.4em;
padding-right: 2px;
}
h1, h2, h3 {
color: #b32d2e !important;
}
.stButton>button {
background-color: #5c1a1b !important;
color: white !important;
border-radius: 5px;
border: none;
}
.stButton>button:hover {
background-color: #732323 !important;
}
</style>
<div class="nav-bar">
<div class="nav-left">
<a class="nav-item" href="/?home=true" target="_self">Home</a>
</div>
<form class="search-form" action="/" method="get">
<input type="text" name="search" class="search-input" placeholder="Search movies...">
<button type="submit" class="search-button">Search</button>
</form>
<div class="nav-right">
<a class="nav-item" href="/?rateflow=true" target="_self">Rate</a>
</div>
</div>
""", unsafe_allow_html=True)
# Modell-Auswahl Dropdown
model_choice = st.radio(
"Choose Recommendation Model:",
options=["Neural Network", "SVD"],
index=0,
horizontal=True,
key="model_selection"
)
query_params = st.query_params
page = query_params.get("rateflow")
search_query = query_params.get("search")
movie_id = query_params.get("movie_id")
def render_star_rating(rating):
return "".join(["<span class='star'>β˜…</span>" for _ in range(rating)])
all_ratings_data = load_ratings()
if movie_id:
try:
movie_id = int(movie_id)
movie_info = movie_df[movie_df["movieId"] == movie_id].iloc[0]
st.title(movie_info["clean_title"])
poster_url, tmdb_link = get_tmdb_data(movie_info["clean_title"], movie_info["year"])
col1, col2 = st.columns([1, 3])
with col1:
if poster_url and "placeholder.com" not in poster_url:
st.image(poster_url, use_container_width=True)
else:
st.markdown("""
<div style='width:100%;border:2px dashed gray;height:450px;display:flex;align-items:center;justify-content:center;color:gray;'>
No picture available
</div>
""", unsafe_allow_html=True)
with col2:
st.subheader("Details")
st.write(f"**Genres:** {movie_info['genres']}")
st.write(f"**Year:** {movie_info['year']}")
if tmdb_link:
st.markdown(f"<a href='{tmdb_link}' target='_blank'>View on TMDb</a>", unsafe_allow_html=True)
rating_key = f"detail_rating_{movie_id}"
user_rating = next((r["rating"] for r in all_ratings_data if r["movie_id"] == movie_id), None)
st.markdown("### Your Rating")
existing_rating = next((r["rating"] for r in all_ratings_data if r["movie_id"] == movie_id), None)
initial_index = existing_rating - 1 if existing_rating else None
new_rating = st.radio("Rate this movie:", [1, 2, 3, 4, 5], horizontal=True, index=initial_index, key=rating_key)
if st.button("Submit Rating"):
if new_rating:
save_rating_to_json({
"movie_id": movie_id,
"rating": new_rating,
"timestamp": datetime.now().isoformat()
})
st.success("Rating saved.")
st.rerun()
except Exception as e:
st.error("Could not load movie details.")
elif page:
st.title("Rate Random Movies")
all_genres = sorted(set(genre.strip() for sublist in movie_df["genres"].dropna().str.split("|") for genre in sublist))
selected_genres = st.multiselect("Select genres:", options=all_genres, placeholder="All genres")
genre_filter = movie_df["genres"].apply(lambda g: any(genre in g.split("|") for genre in selected_genres)) if selected_genres else True
if isinstance(genre_filter, pd.Series):
filtered_df = movie_df[genre_filter].copy()
else:
filtered_df = movie_df.copy()
if filtered_df.empty:
st.warning("No movies match the selected filters.")
st.stop()
if "last_filter_state" not in st.session_state or st.session_state.last_filter_state != tuple(selected_genres):
st.session_state.movie_pool = filtered_df.sample(frac=1).reset_index(drop=True)
st.session_state.random_index = 0
st.session_state.last_filter_state = tuple(selected_genres)
st.session_state.current_random_movie = None
if "movie_pool" not in st.session_state or st.session_state.movie_pool.empty:
st.warning("Movie pool is empty. Try adjusting your filters.")
st.stop()
movie_pool = st.session_state.movie_pool
index = st.session_state.get("random_index", 0)
if index >= len(movie_pool):
st.info("No more movies to rate.")
st.stop()
try:
movie = movie_pool.iloc[index]
except IndexError:
st.error("Could not load a movie. Please try again.")
st.stop()
if movie is None or pd.isna(movie["clean_title"]):
st.error("Invalid movie data.")
st.stop()
poster_url, tmdb_link = get_tmdb_data(movie["clean_title"], movie["year"])
col1, col2 = st.columns([1, 2])
with col1:
if poster_url and "placeholder.com" not in poster_url:
st.image(poster_url, width=300)
else:
st.markdown("""
<div style='width:300px;border:2px dashed gray;height:450px;display:flex;align-items:center;justify-content:center;color:gray;'>
No picture available
</div>
""", unsafe_allow_html=True)
with col2:
st.subheader(movie["clean_title"])
st.markdown(f"**Genres:** {movie['genres']}")
st.markdown(f"**Year:** {movie['year']}")
if tmdb_link:
st.markdown(f"<a href='{tmdb_link}' target='_blank'>View on TMDb</a>", unsafe_allow_html=True)
st.markdown(f"### Movie {index + 1} of {len(movie_pool)}")
st.markdown("### Your Rating")
rating_key = f"rating_{movie['movieId']}"
if rating_key not in st.session_state:
st.session_state[rating_key] = None
st.session_state[rating_key] = st.radio(
"Rate this movie:", [1, 2, 3, 4, 5], horizontal=True, index=None, key=f"radio_{movie['movieId']}")
col_submit, col_skip = st.columns([1, 1])
with col_submit:
rating = st.session_state[rating_key]
if st.button("Submit Rating") and rating:
save_rating_to_json({
"movie_id": int(movie["movieId"]),
"rating": rating,
"timestamp": datetime.now().isoformat()
})
st.session_state["random_index"] = index + 1
st.session_state["just_submitted"] = True
st.rerun()
with col_skip:
if st.button("Didn't Watch"):
st.session_state["random_index"] = index + 1
st.session_state["just_submitted"] = True
st.rerun()
elif search_query:
st.title(f"Search Results for '{search_query}'")
search_clean = search_query.strip().lower()
def title_match_score(title):
title_lower = title.lower()
if title_lower == search_clean:
return 3
elif title_lower.startswith(search_clean):
return 2
elif search_clean in title_lower:
return 1
else:
return 0
movie_df["match_score"] = movie_df["clean_title"].apply(title_match_score)
strong_matches = movie_df[movie_df["match_score"] > 0].sort_values("match_score", ascending=False)
if strong_matches.empty:
close_titles = difflib.get_close_matches(search_query, movie_df["clean_title"], n=25, cutoff=0.5)
filtered = movie_df[movie_df["clean_title"].isin(close_titles)].head(25)
else:
filtered = strong_matches.head(25)
if filtered.empty:
st.warning("No movies found.")
else:
st.markdown("""
<style>
.poster {
width: 100px;
height: 150px;
flex-shrink: 0;
border-radius: 4px;
object-fit: cover;
background: #333;
}
.movie-box {
background-color: #1a1a1a;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
display: flex;
align-items: flex-start;
gap: 20px;
}
.movie-box:hover {
background-color: #262626;
}
.poster {
width: 100px;
height: 150px;
flex-shrink: 0;
border-radius: 4px;
object-fit: cover;
background: #333;
}
.movie-content {
flex-grow: 1;
}
.movie-title {
font-size: 20px;
font-weight: bold;
color: #e63946;
margin-bottom: 0.5rem;
}
.movie-details {
color: #ccc;
font-size: 15px;
margin-bottom: 0.5rem;
}
a.movie-link {
color: #b32d2e;
text-decoration: none;
}
a.movie-link:hover {
text-decoration: underline;
}
</style>
""", unsafe_allow_html=True)
for _, movie in filtered.iterrows():
poster_url, _ = get_tmdb_data(movie["clean_title"], movie["year"])
poster_html = (
f"<img src='{poster_url}' class='poster' alt='Poster'>" if poster_url and "placeholder.com" not in poster_url
else "<div style='width:100px;height:150px;border:2px dashed gray;display:flex;align-items:center;justify-content:center;color:gray;font-size:12px;'>No<br>Image</div>"
)
st.markdown(f"""
<div class="movie-box">
{poster_html}
<div class="movie-content">
<div class="movie-title">
<a href='/?movie_id={movie["movieId"]}' class="movie-link">{movie['clean_title']}</a>
</div>
<div class="movie-details">
<p><strong>Genres:</strong> {movie['genres']}</p>
<p><strong>Year:</strong> {movie['year']}</p>
</div>
</div>
</div>
""", unsafe_allow_html=True)
else:
st.title("Welcome to Movie Recommender")
if not all_ratings_data:
st.info("No ratings available yet. Start rating some movies!")
else:
ratings_df = pd.DataFrame(all_ratings_data)
ratings_df["timestamp"] = pd.to_datetime(ratings_df["timestamp"])
merged = pd.merge(ratings_df, movie_df, left_on="movie_id", right_on="movieId")
def make_clickable_title(row):
return f"<a href='/?movie_id={row['movieId']}' target='_self'>{row['clean_title']}</a>"
def show_table(dataframe, label, checkbox_key):
show_all = st.checkbox(f"Show all in {label}", key=checkbox_key)
st.subheader(label)
display_df = dataframe.copy()
if not show_all:
display_df = display_df.head(5)
if display_df.empty:
st.caption("No entries.")
return
df_display = display_df[["movieId", "clean_title", "rating", "genres", "year", "timestamp"]].copy()
df_display["Title"] = df_display.apply(
lambda row: f"<a href='/?movie_id={row['movieId']}' target='_self' style='color:#e63946;text-decoration:none;'>{row['clean_title']}</a>",
axis=1
)
df_display["Rated"] = df_display["rating"].apply(render_star_rating)
df_display["Date"] = df_display["timestamp"].dt.strftime("%Y-%m-%d")
df_display = df_display[["Title", "Rated", "genres", "year", "Date"]]
st.markdown("""
<style>
.styled-table {
width: 100%;
border-collapse: collapse;
font-size: 16px;
font-family: 'Segoe UI', sans-serif;
background-color: #1a1a1a;
color: #f0f0f0;
border-radius: 8px;
overflow: hidden;
margin-bottom: 2em;
}
.styled-table thead tr {
background-color: #5c1a1b;
text-align: left;
}
.styled-table th, .styled-table td {
padding: 12px 15px;
}
.styled-table tbody tr {
border-bottom: 1px solid #333;
}
.styled-table tbody tr:hover {
background-color: #2a2a2a;
}
</style>
""", unsafe_allow_html=True)
html_table = df_display.to_html(classes='styled-table', escape=False, index=False)
st.markdown(html_table, unsafe_allow_html=True)
# Show all tables
recent = merged.sort_values("timestamp", ascending=False)
show_table(recent, "πŸ•“ Recently Rated", checkbox_key="recently_rated")
top = merged[merged["rating"] >= 4].sort_values(["rating", "timestamp"], ascending=[False, False])
show_table(top, "🌟 Top Rated", checkbox_key="top_rated")
worst = merged[merged["rating"] <= 2].sort_values(["rating", "timestamp"], ascending=[True, False])
show_table(worst, "😞 Worst Rated", checkbox_key="worst_rated")