Made better, and now actualt work

This commit is contained in:
OusmBlueNinja 2025-04-03 15:13:40 -05:00
parent 914c085cac
commit 0a97556f5e
13 changed files with 369 additions and 2013 deletions

458
app.py
View File

@ -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 TFIDF 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 010 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 (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.
@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

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"
# 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
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 %}