Compare commits

..

2 Commits

Author SHA1 Message Date
OusmBlueNinja
08038d165d Update users.db 2025-04-03 15:14:15 -05:00
OusmBlueNinja
0a97556f5e Made better, and now actualt work 2025-04-03 15:13:40 -05:00
13 changed files with 369 additions and 2013 deletions

466
app.py
View File

@ -1,273 +1,237 @@
from flask import Flask, request, render_template, redirect, url_for, session import json, math, random
import json from flask import Flask, render_template, request, redirect, url_for, session, flash
import numpy as np from flask_sqlalchemy import SQLAlchemy
import random from werkzeug.security import generate_password_hash, check_password_hash
import math
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# Initialize Flask and database
app = Flask(__name__) 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 # -------------------- Models --------------------
with open('top_movies.json', 'r', encoding='utf-8') as f: class User(db.Model):
movies = json.load(f) 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 def set_password(self, password):
for i, movie in enumerate(movies): self.password_hash = generate_password_hash(password)
movie['id'] = i # Unique ID
# Combine genres and tags into one feature string. def check_password(self, password):
movie['features'] = ' '.join(movie.get('genres', [])) + ' ' + ' '.join(movie.get('tags', [])) return check_password_hash(self.password_hash, password)
# Ensure numeric values for year and runtime:
# Create the database tables (if needed)
with app.app_context():
db.create_all()
# -------------------- Movie Data & Class --------------------
class Movie:
__slots__ = ('title', 'year', 'imdb_rating', 'runtime', 'features', 'description', 'poster')
def __init__(self, data):
self.title = data.get("title", "")
try: try:
movie['year_num'] = int(movie.get('year', '0')) self.year = int(data.get("year", 0))
except: except (ValueError, TypeError):
movie['year_num'] = 0 self.year = 0
try: try:
movie['runtime_num'] = float(movie.get('runtime')) if movie.get('runtime') else 0 self.imdb_rating = float(data.get("imdb_rating", 0))
except: except (ValueError, TypeError):
movie['runtime_num'] = 0 self.imdb_rating = 0.0
# Ensure vote_count is numeric.
try: try:
count = movie.get('vote_count', 0) self.runtime = int(data.get("runtime", 0))
if isinstance(count, str): except (ValueError, TypeError):
count = count.replace(',', '') self.runtime = 0
if 'M' in count: # For genres/tags, use a frozenset for fast membership testing.
count = float(count.replace('M', '')) * 1e6 genres = data.get("genres", [])
else: tags = data.get("tags", [])
count = int(count) self.features = frozenset(genres + tags)
movie['vote_count'] = int(count) self.description = data.get("description", "")
self.poster = data.get("poster", "")
# 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
# -------------------- 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):
"""
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.
"""
rated_titles = set(profile.keys())
candidates = [m for m in MOVIES if m.title not in rated_titles]
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: except:
movie['vote_count'] = 0 return {}
return session.get('profile', {})
# Build the TFIDF vectorizer on movie features. def save_profile(profile):
vectorizer = TfidfVectorizer(stop_words='english') """Save the profile to the database (if logged in) or session."""
movie_features = [movie['features'] for movie in movies] if 'user_id' in session:
movie_vectors = vectorizer.fit_transform(movie_features) user = User.query.get(session['user_id'])
if user:
# Precompute overall ranges for numeric features. user.profile = pyjson.dumps(profile)
years = [m['year_num'] for m in movies if m['year_num'] > 0] db.session.commit()
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
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 010 scale
def get_predicted_movies(num=10):
"""
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.
"""
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]
# 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))
else: else:
liked_profile = np.zeros((1, movie_vectors.shape[1])) session['profile'] = profile
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
# -------------------- Routes --------------------
@app.route('/') @app.route('/')
def home(): def index():
session.setdefault('rated_movies', {}) # {movie_id: rating} if 'user_id' in session:
session.setdefault('asked_movies', []) # list of movie IDs already shown return redirect(url_for('survey'))
return redirect(url_for('questionnaire')) return redirect(url_for('login'))
@app.route('/questionnaire', methods=['GET', 'POST']) # ----- User Login/Register -----
def questionnaire(): @app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST': if request.method == 'POST':
current_ids = request.form.getlist("movie_id") username = request.form.get('username').strip()
for movie_id in current_ids: password = request.form.get('password')
rating = request.form.get(f"rating_{movie_id}") user = User.query.filter_by(username=username).first()
session['rated_movies'][movie_id] = rating if user and user.check_password(password):
if int(movie_id) not in session['asked_movies']: session['user_id'] = user.id
session['asked_movies'].append(int(movie_id)) flash("Logged in successfully.", "success")
remaining = [m for m in movies if m['id'] not in session['asked_movies']] return redirect(url_for('survey'))
if enough_info() or not remaining: else:
flash("Invalid username or password.", "danger")
return render_template('login.html')
@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')
@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')) return redirect(url_for('recommend'))
else: else:
return redirect(url_for('questionnaire')) # Show 10 random movies for the survey.
else: sample_movies = random.sample(MOVIES, min(10, len(MOVIES)))
# Use prediction to select movies for the questionnaire. return render_template('survey.html', movies=sample_movies)
selected_movies = get_predicted_movies(num=10)
if not selected_movies:
return redirect(url_for('recommend'))
return render_template('questionnaire.html', movies=selected_movies)
def advanced_recommendations():
"""
Compute an advanced hybrid recommendation score on unseen movies.
Only movies not already shown (asked) are considered.
Combines:
1. Text similarity (TFIDF) 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.
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))
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]
@app.route('/recommend') @app.route('/recommend')
def recommend(): def recommend():
recommendations = advanced_recommendations() profile = get_profile()
return render_template('recommendations.html', recommendations=recommendations) 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__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

BIN
instance/users.db Normal file

Binary file not shown.

1584
out.html

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ import concurrent.futures
api_key = "96f3424d6fe55c2982e6e094416607f5" api_key = "96f3424d6fe55c2982e6e094416607f5"
# Output file where results are saved incrementally # Output file where results are saved incrementally
output_filename = "top_movies.json" output_filename = "movies.json"
def write_movies(movies, filename=output_filename): def write_movies(movies, filename=output_filename):
"""Helper function to write the movies list to a JSON file.""" """Helper function to write the movies list to a JSON file."""

48
templates/base.html Normal file
View 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">&times;</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>

View File

@ -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
View 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 %}

View File

@ -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
View 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 %}

View File

@ -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
View 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
View 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 %}