Made better, and now actualt work
This commit is contained in:
parent
914c085cac
commit
0a97556f5e
458
app.py
458
app.py
@ -1,273 +1,237 @@
|
||||
from flask import Flask, request, render_template, redirect, url_for, session
|
||||
import json
|
||||
import numpy as np
|
||||
import random
|
||||
import math
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
import json, math, random
|
||||
from flask import Flask, render_template, request, redirect, url_for, session, flash
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# Initialize Flask and database
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'your_secret_key_here' # Replace with a secure key in production
|
||||
app.config['SECRET_KEY'] = 'your_secret_key' # Replace with a secure secret key
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Load movies from top_movies.json with UTF-8 encoding
|
||||
with open('top_movies.json', 'r', encoding='utf-8') as f:
|
||||
movies = json.load(f)
|
||||
# -------------------- Models --------------------
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(150), nullable=False)
|
||||
# Store the profile as a JSON string mapping movie title to rating.
|
||||
profile = db.Column(db.Text, nullable=False, default='{}')
|
||||
|
||||
# Preprocess each movie
|
||||
for i, movie in enumerate(movies):
|
||||
movie['id'] = i # Unique ID
|
||||
# Combine genres and tags into one feature string.
|
||||
movie['features'] = ' '.join(movie.get('genres', [])) + ' ' + ' '.join(movie.get('tags', []))
|
||||
# Ensure numeric values for year and runtime:
|
||||
try:
|
||||
movie['year_num'] = int(movie.get('year', '0'))
|
||||
except:
|
||||
movie['year_num'] = 0
|
||||
try:
|
||||
movie['runtime_num'] = float(movie.get('runtime')) if movie.get('runtime') else 0
|
||||
except:
|
||||
movie['runtime_num'] = 0
|
||||
# Ensure vote_count is numeric.
|
||||
try:
|
||||
count = movie.get('vote_count', 0)
|
||||
if isinstance(count, str):
|
||||
count = count.replace(',', '')
|
||||
if 'M' in count:
|
||||
count = float(count.replace('M', '')) * 1e6
|
||||
else:
|
||||
count = int(count)
|
||||
movie['vote_count'] = int(count)
|
||||
except:
|
||||
movie['vote_count'] = 0
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
# Build the TF‑IDF vectorizer on movie features.
|
||||
vectorizer = TfidfVectorizer(stop_words='english')
|
||||
movie_features = [movie['features'] for movie in movies]
|
||||
movie_vectors = vectorizer.fit_transform(movie_features)
|
||||
# Create the database tables (if needed)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Precompute overall ranges for numeric features.
|
||||
years = [m['year_num'] for m in movies if m['year_num'] > 0]
|
||||
runtimes = [m['runtime_num'] for m in movies if m['runtime_num'] > 0]
|
||||
max_vote = max([m['vote_count'] for m in movies]) if movies else 1
|
||||
# -------------------- Movie Data & Class --------------------
|
||||
class Movie:
|
||||
__slots__ = ('title', 'year', 'imdb_rating', 'runtime', 'features', 'description', 'poster')
|
||||
def __init__(self, data):
|
||||
self.title = data.get("title", "")
|
||||
try:
|
||||
self.year = int(data.get("year", 0))
|
||||
except (ValueError, TypeError):
|
||||
self.year = 0
|
||||
try:
|
||||
self.imdb_rating = float(data.get("imdb_rating", 0))
|
||||
except (ValueError, TypeError):
|
||||
self.imdb_rating = 0.0
|
||||
try:
|
||||
self.runtime = int(data.get("runtime", 0))
|
||||
except (ValueError, TypeError):
|
||||
self.runtime = 0
|
||||
# For genres/tags, use a frozenset for fast membership testing.
|
||||
genres = data.get("genres", [])
|
||||
tags = data.get("tags", [])
|
||||
self.features = frozenset(genres + tags)
|
||||
self.description = data.get("description", "")
|
||||
self.poster = data.get("poster", "")
|
||||
|
||||
min_year, max_year = (min(years), max(years)) if years else (0, 1)
|
||||
min_runtime, max_runtime = (min(runtimes), max(runtimes)) if runtimes else (0, 1)
|
||||
year_range = max_year - min_year if max_year != min_year else 1
|
||||
runtime_range = max_runtime - min_runtime if max_runtime != min_runtime else 1
|
||||
rating_range = 10.0 # Assuming ratings are on a 0–10 scale
|
||||
# Preload movies from JSON once.
|
||||
MOVIES = []
|
||||
MOVIE_INDEX = {} # mapping from title to Movie object
|
||||
with open('movies.json', 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for entry in data:
|
||||
movie = Movie(entry)
|
||||
MOVIES.append(movie)
|
||||
MOVIE_INDEX[movie.title] = movie
|
||||
|
||||
def get_predicted_movies(num=10):
|
||||
# -------------------- Similarity Functions --------------------
|
||||
def jaccard_similarity(set1, set2):
|
||||
if not set1 and not set2:
|
||||
return 0
|
||||
inter = len(set1 & set2)
|
||||
union = len(set1 | set2)
|
||||
return inter / union if union else 0
|
||||
|
||||
def advanced_similarity(movie1, movie2, weights=None):
|
||||
if weights is None:
|
||||
weights = {
|
||||
"genres_tags": 0.5,
|
||||
"imdb": 0.2,
|
||||
"year": 0.15,
|
||||
"runtime": 0.15
|
||||
}
|
||||
# Genres/tags similarity via Jaccard index.
|
||||
sim_genres = jaccard_similarity(movie1.features, movie2.features)
|
||||
# IMDb rating similarity: difference normalized over a 10-point scale.
|
||||
sim_imdb = max(0, 1 - abs(movie1.imdb_rating - movie2.imdb_rating) / 10)
|
||||
# Year similarity using exponential decay.
|
||||
sim_year = math.exp(-abs(movie1.year - movie2.year) / 10)
|
||||
# Runtime similarity using exponential decay.
|
||||
sim_runtime = math.exp(-abs(movie1.runtime - movie2.runtime) / 30)
|
||||
overall = (weights["genres_tags"] * sim_genres +
|
||||
weights["imdb"] * sim_imdb +
|
||||
weights["year"] * sim_year +
|
||||
weights["runtime"] * sim_runtime)
|
||||
return overall
|
||||
|
||||
def recommend_movie(profile, weights=None):
|
||||
"""
|
||||
Return up to `num` movies that haven't been shown yet.
|
||||
Uses the user's past ratings to predict which unseen movies they might like.
|
||||
If no ratings exist, falls back to random selection.
|
||||
Given a user profile (dict mapping movie title to rating), return one recommended Movie.
|
||||
The function iterates over candidate movies (those not rated yet) and computes an aggregated
|
||||
weighted similarity score against all rated movies.
|
||||
"""
|
||||
asked = session.get('asked_movies', [])
|
||||
available = [m for m in movies if m['id'] not in asked]
|
||||
if not available:
|
||||
return []
|
||||
rated = session.get('rated_movies', {})
|
||||
# Fallback to random selection if there are no like/dislike ratings.
|
||||
if not rated or not any(r in ['like', 'dislike'] for r in rated.values()):
|
||||
random.shuffle(available)
|
||||
return available[:num]
|
||||
rated_titles = set(profile.keys())
|
||||
candidates = [m for m in MOVIES if m.title not in rated_titles]
|
||||
|
||||
# Build prediction profiles.
|
||||
liked_ids = [int(mid) for mid, rating in rated.items() if rating == 'like']
|
||||
disliked_ids = [int(mid) for mid, rating in rated.items() if rating == 'dislike']
|
||||
|
||||
if liked_ids:
|
||||
liked_profile = np.asarray(movie_vectors[liked_ids].mean(axis=0))
|
||||
best_movie = None
|
||||
best_score = -1
|
||||
for candidate in candidates:
|
||||
weighted_scores = []
|
||||
for title, rating in profile.items():
|
||||
# Skip movies not seen (rating 0)
|
||||
if rating == 0:
|
||||
continue
|
||||
rated_movie = MOVIE_INDEX.get(title)
|
||||
if not rated_movie:
|
||||
continue
|
||||
# Normalize rating: 1 becomes 0 and 5 becomes 1.
|
||||
norm_rating = (rating - 1) / 4
|
||||
sim = advanced_similarity(candidate, rated_movie, weights=weights)
|
||||
weighted_scores.append(norm_rating * sim)
|
||||
avg_score = sum(weighted_scores) / len(weighted_scores) if weighted_scores else 0
|
||||
if avg_score > best_score:
|
||||
best_score = avg_score
|
||||
best_movie = candidate
|
||||
return best_movie
|
||||
|
||||
# -------------------- Helper Functions --------------------
|
||||
import json as pyjson
|
||||
|
||||
def get_profile():
|
||||
"""Return the current user's profile as a dict.
|
||||
If not logged in, use session storage."""
|
||||
if 'user_id' in session:
|
||||
user = User.query.get(session['user_id'])
|
||||
if user:
|
||||
try:
|
||||
return pyjson.loads(user.profile)
|
||||
except:
|
||||
return {}
|
||||
return session.get('profile', {})
|
||||
|
||||
def save_profile(profile):
|
||||
"""Save the profile to the database (if logged in) or session."""
|
||||
if 'user_id' in session:
|
||||
user = User.query.get(session['user_id'])
|
||||
if user:
|
||||
user.profile = pyjson.dumps(profile)
|
||||
db.session.commit()
|
||||
else:
|
||||
liked_profile = np.zeros((1, movie_vectors.shape[1]))
|
||||
if disliked_ids:
|
||||
disliked_profile = np.asarray(movie_vectors[disliked_ids].mean(axis=0))
|
||||
else:
|
||||
disliked_profile = np.zeros((1, movie_vectors.shape[1]))
|
||||
|
||||
# Compute numeric averages for liked movies.
|
||||
liked_years = [movies[i]['year_num'] for i in liked_ids if movies[i]['year_num'] > 0]
|
||||
liked_runtimes = [movies[i]['runtime_num'] for i in liked_ids if movies[i]['runtime_num'] > 0]
|
||||
liked_ratings = [movies[i].get('imdb_rating', 0) for i in liked_ids if movies[i].get('imdb_rating', 0)]
|
||||
|
||||
avg_year = np.mean(liked_years) if liked_years else None
|
||||
avg_runtime = np.mean(liked_runtimes) if liked_runtimes else None
|
||||
avg_rating = np.mean(liked_ratings) if liked_ratings else None
|
||||
|
||||
predictions = []
|
||||
# Tunable weights.
|
||||
w_text = 0.5
|
||||
w_year = 0.1
|
||||
w_runtime = 0.1
|
||||
w_rating = 0.15
|
||||
w_popularity = 0.15
|
||||
|
||||
for movie in available:
|
||||
i = movie['id']
|
||||
# TEXT SIMILARITY.
|
||||
movie_vector = movie_vectors[i].toarray()
|
||||
like_sim = cosine_similarity(movie_vector, liked_profile)[0][0] if np.linalg.norm(liked_profile) != 0 else 0
|
||||
dislike_sim = cosine_similarity(movie_vector, disliked_profile)[0][0] if np.linalg.norm(disliked_profile) != 0 else 0
|
||||
text_score = like_sim - dislike_sim
|
||||
|
||||
# YEAR SIMILARITY.
|
||||
year_score = 0
|
||||
if avg_year is not None and movie['year_num'] > 0:
|
||||
diff_year = abs(movie['year_num'] - avg_year)
|
||||
year_score = 1 - (diff_year / year_range)
|
||||
|
||||
# RUNTIME SIMILARITY.
|
||||
runtime_score = 0
|
||||
if avg_runtime is not None and movie['runtime_num'] > 0:
|
||||
diff_runtime = abs(movie['runtime_num'] - avg_runtime)
|
||||
runtime_score = 1 - (diff_runtime / runtime_range)
|
||||
|
||||
# RATING SIMILARITY.
|
||||
rating_score = 0
|
||||
movie_rating = movie.get('imdb_rating', 0)
|
||||
if avg_rating is not None and movie_rating:
|
||||
diff_rating = abs(movie_rating - avg_rating)
|
||||
rating_score = 1 - (diff_rating / rating_range)
|
||||
|
||||
# POPULARITY SCORE.
|
||||
popularity_score = 0
|
||||
if movie['vote_count'] > 0:
|
||||
popularity_score = math.log(movie['vote_count'] + 1) / math.log(max_vote + 1)
|
||||
|
||||
# Final prediction score.
|
||||
final_score = (w_text * text_score +
|
||||
w_year * year_score +
|
||||
w_runtime * runtime_score +
|
||||
w_rating * rating_score +
|
||||
w_popularity * popularity_score)
|
||||
predictions.append((movie, final_score))
|
||||
|
||||
predictions.sort(key=lambda x: x[1], reverse=True)
|
||||
return [pred[0] for pred in predictions[:num]]
|
||||
|
||||
def enough_info():
|
||||
"""
|
||||
Check if the user has rated at least 3 movies (like/dislike).
|
||||
"""
|
||||
rated = session.get('rated_movies', {})
|
||||
count = sum(1 for rating in rated.values() if rating in ['like', 'dislike'])
|
||||
return count >= 3
|
||||
session['profile'] = profile
|
||||
|
||||
# -------------------- Routes --------------------
|
||||
@app.route('/')
|
||||
def home():
|
||||
session.setdefault('rated_movies', {}) # {movie_id: rating}
|
||||
session.setdefault('asked_movies', []) # list of movie IDs already shown
|
||||
return redirect(url_for('questionnaire'))
|
||||
def index():
|
||||
if 'user_id' in session:
|
||||
return redirect(url_for('survey'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/questionnaire', methods=['GET', 'POST'])
|
||||
def questionnaire():
|
||||
# ----- User Login/Register -----
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
current_ids = request.form.getlist("movie_id")
|
||||
for movie_id in current_ids:
|
||||
rating = request.form.get(f"rating_{movie_id}")
|
||||
session['rated_movies'][movie_id] = rating
|
||||
if int(movie_id) not in session['asked_movies']:
|
||||
session['asked_movies'].append(int(movie_id))
|
||||
remaining = [m for m in movies if m['id'] not in session['asked_movies']]
|
||||
if enough_info() or not remaining:
|
||||
return redirect(url_for('recommend'))
|
||||
username = request.form.get('username').strip()
|
||||
password = request.form.get('password')
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.check_password(password):
|
||||
session['user_id'] = user.id
|
||||
flash("Logged in successfully.", "success")
|
||||
return redirect(url_for('survey'))
|
||||
else:
|
||||
return redirect(url_for('questionnaire'))
|
||||
else:
|
||||
# Use prediction to select movies for the questionnaire.
|
||||
selected_movies = get_predicted_movies(num=10)
|
||||
if not selected_movies:
|
||||
return redirect(url_for('recommend'))
|
||||
return render_template('questionnaire.html', movies=selected_movies)
|
||||
flash("Invalid username or password.", "danger")
|
||||
return render_template('login.html')
|
||||
|
||||
def advanced_recommendations():
|
||||
"""
|
||||
Compute an advanced hybrid recommendation score on unseen movies.
|
||||
Only movies not already shown (asked) are considered.
|
||||
Combines:
|
||||
1. Text similarity (TF‑IDF) between liked/disliked profiles.
|
||||
2. Year similarity.
|
||||
3. Runtime similarity.
|
||||
4. Rating similarity.
|
||||
5. Popularity (log-scaled vote count).
|
||||
Returns the top 20 recommendations.
|
||||
"""
|
||||
rated = session.get('rated_movies', {})
|
||||
asked = set(session.get('asked_movies', []))
|
||||
# Only consider movies that haven't been shown to the user.
|
||||
available = [m for m in movies if m['id'] not in asked]
|
||||
if not available:
|
||||
available = movies # Fallback if all movies have been shown.
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username').strip()
|
||||
password = request.form.get('password')
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash("Username already exists.", "danger")
|
||||
else:
|
||||
user = User(username=username)
|
||||
user.set_password(password)
|
||||
user.profile = pyjson.dumps({})
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash("Registration successful. Please log in.", "success")
|
||||
return redirect(url_for('login'))
|
||||
return render_template('register.html')
|
||||
|
||||
liked_ids = [int(mid) for mid, rating in rated.items() if rating == 'like']
|
||||
disliked_ids = [int(mid) for mid, rating in rated.items() if rating == 'dislike']
|
||||
|
||||
if liked_ids:
|
||||
liked_profile = np.asarray(movie_vectors[liked_ids].mean(axis=0))
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
flash("Logged out.", "info")
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# ----- Survey & Recommendation -----
|
||||
@app.route('/survey', methods=['GET', 'POST'])
|
||||
def survey():
|
||||
if request.method == 'POST':
|
||||
profile = {}
|
||||
for key, value in request.form.items():
|
||||
if key.startswith("rating_"):
|
||||
movie_title = key[len("rating_"):]
|
||||
try:
|
||||
rating = float(value)
|
||||
except (ValueError, TypeError):
|
||||
rating = 0
|
||||
profile[movie_title] = rating
|
||||
# Merge with any existing profile.
|
||||
current_profile = get_profile()
|
||||
current_profile.update(profile)
|
||||
save_profile(current_profile)
|
||||
return redirect(url_for('recommend'))
|
||||
else:
|
||||
liked_profile = np.zeros((1, movie_vectors.shape[1]))
|
||||
if disliked_ids:
|
||||
disliked_profile = np.asarray(movie_vectors[disliked_ids].mean(axis=0))
|
||||
else:
|
||||
disliked_profile = np.zeros((1, movie_vectors.shape[1]))
|
||||
|
||||
liked_years = [movies[i]['year_num'] for i in liked_ids if movies[i]['year_num'] > 0]
|
||||
liked_runtimes = [movies[i]['runtime_num'] for i in liked_ids if movies[i]['runtime_num'] > 0]
|
||||
liked_ratings = [movies[i].get('imdb_rating', 0) for i in liked_ids if movies[i].get('imdb_rating', 0)]
|
||||
avg_year = np.mean(liked_years) if liked_years else None
|
||||
avg_runtime = np.mean(liked_runtimes) if liked_runtimes else None
|
||||
avg_rating = np.mean(liked_ratings) if liked_ratings else None
|
||||
|
||||
recommendations = []
|
||||
w_text = 0.5
|
||||
w_year = 0.1
|
||||
w_runtime = 0.1
|
||||
w_rating = 0.15
|
||||
w_popularity = 0.15
|
||||
|
||||
for movie in available:
|
||||
i = movie['id']
|
||||
movie_vector = movie_vectors[i].toarray()
|
||||
like_sim = cosine_similarity(movie_vector, liked_profile)[0][0] if np.linalg.norm(liked_profile) != 0 else 0
|
||||
dislike_sim = cosine_similarity(movie_vector, disliked_profile)[0][0] if np.linalg.norm(disliked_profile) != 0 else 0
|
||||
text_score = like_sim - dislike_sim
|
||||
|
||||
year_score = 0
|
||||
if avg_year is not None and movie['year_num'] > 0:
|
||||
diff_year = abs(movie['year_num'] - avg_year)
|
||||
year_score = 1 - (diff_year / year_range)
|
||||
|
||||
runtime_score = 0
|
||||
if avg_runtime is not None and movie['runtime_num'] > 0:
|
||||
diff_runtime = abs(movie['runtime_num'] - avg_runtime)
|
||||
runtime_score = 1 - (diff_runtime / runtime_range)
|
||||
|
||||
rating_score = 0
|
||||
movie_rating = movie.get('imdb_rating', 0)
|
||||
if avg_rating is not None and movie_rating:
|
||||
diff_rating = abs(movie_rating - avg_rating)
|
||||
rating_score = 1 - (diff_rating / rating_range)
|
||||
|
||||
popularity_score = 0
|
||||
if movie['vote_count'] > 0:
|
||||
popularity_score = math.log(movie['vote_count'] + 1) / math.log(max_vote + 1)
|
||||
|
||||
final_score = (w_text * text_score +
|
||||
w_year * year_score +
|
||||
w_runtime * runtime_score +
|
||||
w_rating * rating_score +
|
||||
w_popularity * popularity_score)
|
||||
recommendations.append((movie, final_score))
|
||||
|
||||
recommendations.sort(key=lambda x: x[1], reverse=True)
|
||||
return recommendations[:20]
|
||||
# Show 10 random movies for the survey.
|
||||
sample_movies = random.sample(MOVIES, min(10, len(MOVIES)))
|
||||
return render_template('survey.html', movies=sample_movies)
|
||||
|
||||
@app.route('/recommend')
|
||||
def recommend():
|
||||
recommendations = advanced_recommendations()
|
||||
return render_template('recommendations.html', recommendations=recommendations)
|
||||
profile = get_profile()
|
||||
movie = recommend_movie(profile)
|
||||
return render_template('recommend.html', movie=movie)
|
||||
|
||||
@app.route('/feedback', methods=['POST'])
|
||||
def feedback():
|
||||
movie_title = request.form.get("movie_title")
|
||||
try:
|
||||
rating = float(request.form.get("rating"))
|
||||
except (ValueError, TypeError):
|
||||
rating = 0
|
||||
profile = get_profile()
|
||||
profile[movie_title] = rating
|
||||
save_profile(profile)
|
||||
return redirect(url_for('recommend'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
|
||||
|
||||
|
BIN
instance/users.db
Normal file
BIN
instance/users.db
Normal file
Binary file not shown.
@ -8,7 +8,7 @@ import concurrent.futures
|
||||
api_key = "96f3424d6fe55c2982e6e094416607f5"
|
||||
|
||||
# Output file where results are saved incrementally
|
||||
output_filename = "top_movies.json"
|
||||
output_filename = "movies.json"
|
||||
|
||||
def write_movies(movies, filename=output_filename):
|
||||
"""Helper function to write the movies list to a JSON file."""
|
48
templates/base.html
Normal file
48
templates/base.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Movie Recommender{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<style>
|
||||
.movie-card { margin-bottom: 20px; }
|
||||
.movie-poster { object-fit: cover; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="{{ url_for('survey') }}">Movie Recommender</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{% if session.get('user_id') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('register') }}">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-4">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,77 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Movie Slideshow</title>
|
||||
<style>
|
||||
/* Basic styling for slideshow */
|
||||
#movie-container {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
#movie-poster {
|
||||
width: 200px;
|
||||
margin: 20px;
|
||||
}
|
||||
.rating-buttons button {
|
||||
margin: 10px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="text-align: center;">Rate Movies</h1>
|
||||
<form id="ratingForm" method="POST" action="/recommend">
|
||||
<!-- Hidden inputs for movie ratings; one per movie -->
|
||||
{% for movie in movies %}
|
||||
<input type="hidden" name="{{ movie.title }}" id="rating-{{ loop.index0 }}" value="not seen">
|
||||
{% endfor %}
|
||||
|
||||
<div id="movie-container">
|
||||
<img id="movie-poster" src="" alt="Movie Poster">
|
||||
<h2 id="movie-title"></h2>
|
||||
<p id="movie-description"></p>
|
||||
</div>
|
||||
|
||||
<div class="rating-buttons" style="text-align: center;">
|
||||
<button type="button" onclick="recordRating('like')">Like</button>
|
||||
<button type="button" onclick="recordRating('dislike')">Dislike</button>
|
||||
<button type="button" onclick="recordRating('not seen')">Not Seen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const movies = {{ movies | tojson }};
|
||||
let currentIndex = 0;
|
||||
const posterEl = document.getElementById("movie-poster");
|
||||
const titleEl = document.getElementById("movie-title");
|
||||
const descriptionEl = document.getElementById("movie-description");
|
||||
|
||||
// Function to display the movie at the given index
|
||||
function showMovie(index) {
|
||||
if (index >= movies.length) {
|
||||
// All movies rated; submit the form
|
||||
document.getElementById("ratingForm").submit();
|
||||
return;
|
||||
}
|
||||
const movie = movies[index];
|
||||
posterEl.src = movie.poster;
|
||||
posterEl.alt = movie.title;
|
||||
titleEl.textContent = movie.title + " (" + movie.year + ")";
|
||||
descriptionEl.textContent = movie.description;
|
||||
}
|
||||
|
||||
// Record the rating for the current movie and show the next one
|
||||
function recordRating(rating) {
|
||||
// Update the hidden input for the current movie with the chosen rating
|
||||
document.getElementById("rating-" + currentIndex).value = rating;
|
||||
currentIndex++;
|
||||
showMovie(currentIndex);
|
||||
}
|
||||
|
||||
// Initialize the slideshow with the first movie
|
||||
showMovie(currentIndex);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
17
templates/login.html
Normal file
17
templates/login.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Login{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Login</h2>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" name="username" id="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
<p class="mt-2">Don't have an account? <a href="{{ url_for('register') }}">Register here</a>.</p>
|
||||
{% endblock %}
|
@ -1,83 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Movie Questionnaire</title>
|
||||
<style>
|
||||
/* Styling for the slideshow */
|
||||
#movie-container {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
#movie-poster {
|
||||
width: 200px;
|
||||
margin: 20px;
|
||||
}
|
||||
.rating-buttons button {
|
||||
margin: 10px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="text-align: center;">Rate Movies</h1>
|
||||
<form id="questionForm" method="POST">
|
||||
<!-- Container where hidden inputs will be added for the round -->
|
||||
<div id="hiddenFields"></div>
|
||||
<div id="movie-container">
|
||||
<img id="movie-poster" src="" alt="Movie Poster">
|
||||
<h2 id="movie-title"></h2>
|
||||
<p id="movie-description"></p>
|
||||
</div>
|
||||
<div class="rating-buttons" style="text-align: center;">
|
||||
<button type="button" onclick="recordRating('like')">Like</button>
|
||||
<button type="button" onclick="recordRating('dislike')">Dislike</button>
|
||||
<button type="button" onclick="recordRating('not seen')">Not Seen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Movies for the current round are passed from the server.
|
||||
const movies = {{ movies | tojson }};
|
||||
let currentIndex = 0;
|
||||
let movieRatings = {}; // To store ratings for this batch
|
||||
|
||||
function showMovie(index) {
|
||||
if (index >= movies.length) {
|
||||
// All movies rated in this round—append hidden fields and submit the form.
|
||||
const container = document.getElementById("hiddenFields");
|
||||
movies.forEach(movie => {
|
||||
// Hidden input for movie id
|
||||
const movieIdInput = document.createElement("input");
|
||||
movieIdInput.type = "hidden";
|
||||
movieIdInput.name = "movie_id";
|
||||
movieIdInput.value = movie.id;
|
||||
container.appendChild(movieIdInput);
|
||||
// Hidden input for its rating
|
||||
const ratingInput = document.createElement("input");
|
||||
ratingInput.type = "hidden";
|
||||
ratingInput.name = "rating_" + movie.id;
|
||||
ratingInput.value = movieRatings[movie.id] || "not seen";
|
||||
container.appendChild(ratingInput);
|
||||
});
|
||||
document.getElementById("questionForm").submit();
|
||||
return;
|
||||
}
|
||||
const movie = movies[currentIndex];
|
||||
document.getElementById("movie-poster").src = movie.poster;
|
||||
document.getElementById("movie-poster").alt = movie.title;
|
||||
document.getElementById("movie-title").textContent = movie.title + " (" + movie.year + ")";
|
||||
document.getElementById("movie-description").textContent = movie.description;
|
||||
}
|
||||
|
||||
function recordRating(rating) {
|
||||
movieRatings[movies[currentIndex].id] = rating;
|
||||
currentIndex++;
|
||||
showMovie(currentIndex);
|
||||
}
|
||||
|
||||
showMovie(currentIndex);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
39
templates/recommend.html
Normal file
39
templates/recommend.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Recommendation{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Your Recommended Movie</h2>
|
||||
{% if movie %}
|
||||
<div class="card movie-card">
|
||||
<div class="row no-gutters">
|
||||
{% if movie.poster %}
|
||||
<div class="col-md-4">
|
||||
<img src="{{ movie.poster }}" class="card-img movie-poster" style="height:400px;" alt="{{ movie.title }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ movie.title }} ({{ movie.year }})</h5>
|
||||
<p class="card-text">{{ movie.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3">Have you seen this movie? If yes, rate it from 1 (disliked) to 5 (loved); otherwise select "Not Seen".</p>
|
||||
<form method="post" action="{{ url_for('feedback') }}">
|
||||
<input type="hidden" name="movie_title" value="{{ movie.title }}">
|
||||
<div class="btn-group btn-group-lg" role="group">
|
||||
<button type="submit" name="rating" value="0" class="btn btn-secondary">Not Seen</button>
|
||||
<button type="submit" name="rating" value="1" class="btn btn-danger">1</button>
|
||||
<button type="submit" name="rating" value="2" class="btn btn-warning">2</button>
|
||||
<button type="submit" name="rating" value="3" class="btn btn-info">3</button>
|
||||
<button type="submit" name="rating" value="4" class="btn btn-primary">4</button>
|
||||
<button type="submit" name="rating" value="5" class="btn btn-success">5</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-info mt-4" role="alert">
|
||||
No more recommendations available. Thank you for your feedback!
|
||||
</div>
|
||||
<a href="{{ url_for('survey') }}" class="btn btn-outline-primary mt-3">Restart Survey</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Movie Recommendations</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Your Movie Recommendations</h1>
|
||||
{% for movie, score in recommendations %}
|
||||
<div style="margin-bottom: 20px;">
|
||||
<img src="{{ movie.poster }}" alt="{{ movie.title }}" width="70" style="vertical-align: middle;" />
|
||||
<strong>{{ movie.title }} ({{ movie.year }})</strong>
|
||||
<p>{{ movie.description }}</p>
|
||||
<a href="{{ movie.url }}" target="_blank">More Info</a>
|
||||
<p>Recommendation Score: {{ score | round(3) }}</p>
|
||||
</div>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<a href="/">Back to Questionnaire</a>
|
||||
</body>
|
||||
</html>
|
17
templates/register.html
Normal file
17
templates/register.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Register{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Register</h2>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" name="username" id="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
<p class="mt-2">Already have an account? <a href="{{ url_for('login') }}">Login here</a>.</p>
|
||||
{% endblock %}
|
36
templates/survey.html
Normal file
36
templates/survey.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Movie Survey{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Initial Movie Survey</h2>
|
||||
<p>Please rate the following movies on a scale from 1 (disliked) to 5 (loved), or choose "Not Seen".</p>
|
||||
<form method="post">
|
||||
{% for movie in movies %}
|
||||
<div class="card movie-card">
|
||||
<div class="row no-gutters">
|
||||
{% if movie.poster %}
|
||||
<div class="col-md-4">
|
||||
<img src="{{ movie.poster }}" class="card-img movie-poster" style="height:300px;" alt="{{ movie.title }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ movie.title }} ({{ movie.year }})</h5>
|
||||
<p class="card-text">{{ movie.description }}</p>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="rating_{{ movie.title }}" id="{{ movie.title }}_0" value="0" checked>
|
||||
<label class="form-check-label" for="{{ movie.title }}_0">Not Seen</label>
|
||||
</div>
|
||||
{% for i in range(1, 6) %}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="rating_{{ movie.title }}" id="{{ movie.title }}_{{ i }}" value="{{ i }}">
|
||||
<label class="form-check-label" for="{{ movie.title }}_{{ i }}">{{ i }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-success btn-lg btn-block">Submit Survey</button>
|
||||
</form>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user