Spaces:
Sleeping
Sleeping
| 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" | |
| 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) | |
| 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") | |