dev2004v commited on
Commit
f2533f4
·
verified ·
1 Parent(s): 7b0530a

Upload 10 files

Browse files
app.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+ from config.database import init_db
4
+ from routes.auth import auth_bp
5
+ from routes.recommend import recommend_bp
6
+ from routes.history import history_bp
7
+ from routes.user import user_bp
8
+ import os
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ app = Flask(__name__)
14
+ app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
15
+
16
+ # Enable CORS
17
+ CORS(app)
18
+
19
+ # Initialize MongoDB
20
+ init_db()
21
+
22
+ # Register blueprints
23
+ app.register_blueprint(auth_bp, url_prefix='/auth')
24
+ app.register_blueprint(recommend_bp)
25
+ app.register_blueprint(history_bp)
26
+ app.register_blueprint(user_bp, url_prefix='/user')
27
+
28
+ @app.route('/health', methods=['GET'])
29
+ def health_check():
30
+ return {"status": "healthy", "message": "Flask backend is running"}, 200
31
+
32
+ if __name__ == "__main__":
33
+ app.run(port=5000, debug=True)
config/database.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import MongoClient
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ client = None
8
+ db = None
9
+
10
+ def init_db():
11
+ global client, db
12
+
13
+ MONGO_URI = os.getenv('MONGO_URI')
14
+ DATABASE_NAME = os.getenv('DATABASE_NAME', 'recommendation_system')
15
+
16
+ if not MONGO_URI:
17
+ raise ValueError("MONGO_URI environment variable is required")
18
+
19
+ try:
20
+ client = MongoClient(MONGO_URI)
21
+ db = client[DATABASE_NAME]
22
+
23
+ # Test connection
24
+ client.admin.command('ping')
25
+ print(f"Connected to MongoDB database: {DATABASE_NAME}")
26
+
27
+ # Create indexes for performance
28
+ db.users.create_index("email", unique=True)
29
+ db.users.create_index("username", unique=True)
30
+ db.history.create_index([("user_id", 1), ("timestamp", -1)])
31
+
32
+ return db
33
+ except Exception as e:
34
+ print(f"Failed to connect to MongoDB: {e}")
35
+ raise e
36
+
37
+ def get_db():
38
+ global db
39
+ if db is None:
40
+ init_db()
41
+ return db
models/history.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timezone
2
+ from config.database import get_db
3
+ from bson import ObjectId
4
+
5
+ class History:
6
+ def __init__(self, user_id, recommendation_type, genre, items, query_params=None):
7
+ self.user_id = ObjectId(user_id)
8
+ self.recommendation_type = recommendation_type
9
+ self.genre = genre
10
+ self.items = items
11
+ self.query_params = query_params or {}
12
+ self.timestamp = datetime.now(timezone.utc)
13
+
14
+ def save(self):
15
+ db = get_db()
16
+ history_data = {
17
+ "user_id": self.user_id,
18
+ "recommendation_type": self.recommendation_type,
19
+ "genre": self.genre,
20
+ "items": self.items,
21
+ "query_params": self.query_params,
22
+ "timestamp": self.timestamp
23
+ }
24
+ result = db.history.insert_one(history_data)
25
+ return str(result.inserted_id)
26
+
27
+ @staticmethod
28
+ def get_user_history(user_id, limit=50, offset=0):
29
+ db = get_db()
30
+ try:
31
+ history = list(db.history.find(
32
+ {"user_id": ObjectId(user_id)}
33
+ ).sort("timestamp", -1).skip(offset).limit(limit))
34
+
35
+ # Convert ObjectId to string for JSON serialization
36
+ for item in history:
37
+ item["_id"] = str(item["_id"])
38
+ item["user_id"] = str(item["user_id"])
39
+
40
+ return history
41
+ except:
42
+ return []
43
+
44
+ @staticmethod
45
+ def get_user_stats(user_id):
46
+ db = get_db()
47
+ try:
48
+ # Type statistics
49
+ type_pipeline = [
50
+ {"$match": {"user_id": ObjectId(user_id)}},
51
+ {"$group": {
52
+ "_id": "$recommendation_type",
53
+ "count": {"$sum": 1},
54
+ "last_accessed": {"$max": "$timestamp"}
55
+ }}
56
+ ]
57
+ type_stats = list(db.history.aggregate(type_pipeline))
58
+
59
+ # Genre preferences - handle both string and array genres
60
+ genre_pipeline = [
61
+ {"$match": {"user_id": ObjectId(user_id)}},
62
+ {"$unwind": "$genre"},
63
+ {"$group": {
64
+ "_id": "$genre",
65
+ "count": {"$sum": 1}
66
+ }},
67
+ {"$sort": {"count": -1}},
68
+ {"$limit": 10}
69
+ ]
70
+ genre_stats = list(db.history.aggregate(genre_pipeline))
71
+
72
+ total_count = db.history.count_documents({"user_id": ObjectId(user_id)})
73
+
74
+ return {
75
+ "type_stats": type_stats,
76
+ "genre_preferences": genre_stats,
77
+ "total_recommendations": total_count
78
+ }
79
+ except:
80
+ return {
81
+ "type_stats": [],
82
+ "genre_preferences": [],
83
+ "total_recommendations": 0
84
+ }
models/user.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timezone
2
+ from werkzeug.security import generate_password_hash, check_password_hash
3
+ from config.database import get_db
4
+ from bson import ObjectId
5
+
6
+ class User:
7
+ def __init__(self, username=None, email=None, password=None, preferences=None):
8
+ self.username = username
9
+ self.email = email
10
+ self.password_hash = generate_password_hash(password) if password else None
11
+ self.preferences = preferences or {"genres": [], "types": []}
12
+ self.created_at = datetime.now(timezone.utc)
13
+ self.last_login = None
14
+
15
+ def save(self):
16
+ db = get_db()
17
+ user_data = {
18
+ "username": self.username,
19
+ "email": self.email,
20
+ "password_hash": self.password_hash,
21
+ "preferences": self.preferences,
22
+ "created_at": self.created_at,
23
+ "last_login": self.last_login
24
+ }
25
+ result = db.users.insert_one(user_data)
26
+ return str(result.inserted_id)
27
+
28
+ @staticmethod
29
+ def find_by_email(email):
30
+ db = get_db()
31
+ user_data = db.users.find_one({"email": email})
32
+ if user_data:
33
+ user = User()
34
+ user._id = user_data["_id"]
35
+ user.username = user_data["username"]
36
+ user.email = user_data["email"]
37
+ user.password_hash = user_data["password_hash"]
38
+ user.preferences = user_data.get("preferences", {"genres": [], "types": []})
39
+ user.created_at = user_data["created_at"]
40
+ user.last_login = user_data.get("last_login")
41
+ return user
42
+ return None
43
+
44
+ @staticmethod
45
+ def find_by_username(username):
46
+ db = get_db()
47
+ user_data = db.users.find_one({"username": username})
48
+ if user_data:
49
+ user = User()
50
+ user._id = user_data["_id"]
51
+ user.username = user_data["username"]
52
+ user.email = user_data["email"]
53
+ user.password_hash = user_data["password_hash"]
54
+ user.preferences = user_data.get("preferences", {"genres": [], "types": []})
55
+ user.created_at = user_data["created_at"]
56
+ user.last_login = user_data.get("last_login")
57
+ return user
58
+ return None
59
+
60
+ @staticmethod
61
+ def find_by_id(user_id):
62
+ db = get_db()
63
+ try:
64
+ user_data = db.users.find_one({"_id": ObjectId(user_id)})
65
+ if user_data:
66
+ user = User()
67
+ user._id = user_data["_id"]
68
+ user.username = user_data["username"]
69
+ user.email = user_data["email"]
70
+ user.password_hash = user_data["password_hash"]
71
+ user.preferences = user_data.get("preferences", {"genres": [], "types": []})
72
+ user.created_at = user_data["created_at"]
73
+ user.last_login = user_data.get("last_login")
74
+ return user
75
+ except:
76
+ pass
77
+ return None
78
+
79
+ def check_password(self, password):
80
+ return check_password_hash(self.password_hash, password)
81
+
82
+ def update_last_login(self):
83
+ db = get_db()
84
+ self.last_login = datetime.now(timezone.utc)
85
+ db.users.update_one(
86
+ {"_id": self._id},
87
+ {"$set": {"last_login": self.last_login}}
88
+ )
89
+
90
+ def update_preferences(self, preferences):
91
+ db = get_db()
92
+ self.preferences = preferences
93
+ db.users.update_one(
94
+ {"_id": self._id},
95
+ {"$set": {"preferences": preferences}}
96
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Flask==2.3.3
2
+ pymongo==4.5.0
3
+ PyJWT==2.8.0
4
+ Werkzeug==2.3.7
5
+ python-dotenv==1.0.0
6
+ requests==2.31.0
7
+ flask-cors==4.0.0
routes/auth.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from models.user import User
3
+ from utils.jwt_helper import generate_jwt
4
+ import re
5
+
6
+ auth_bp = Blueprint('auth', __name__)
7
+
8
+ def validate_email(email):
9
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
10
+ return re.match(pattern, email) is not None
11
+
12
+ def validate_password(password):
13
+ return len(password) >= 6
14
+
15
+ @auth_bp.route('/signup', methods=['POST'])
16
+ def signup():
17
+ try:
18
+ data = request.get_json()
19
+
20
+ if not data:
21
+ return jsonify({"error": "No data provided"}), 400
22
+
23
+ username = data.get('username', '').strip()
24
+ email = data.get('email', '').strip().lower()
25
+ password = data.get('password', '')
26
+ preferences = data.get('preferences', {"genres": [], "types": []})
27
+
28
+ # Validation
29
+ if not username or not email or not password:
30
+ return jsonify({"error": "Username, email, and password are required"}), 400
31
+
32
+ if len(username) < 3:
33
+ return jsonify({"error": "Username must be at least 3 characters long"}), 400
34
+
35
+ if not validate_email(email):
36
+ return jsonify({"error": "Invalid email format"}), 400
37
+
38
+ if not validate_password(password):
39
+ return jsonify({"error": "Password must be at least 6 characters long"}), 400
40
+
41
+ # Check if user already exists
42
+ if User.find_by_email(email):
43
+ return jsonify({"error": "Email already registered"}), 409
44
+
45
+ if User.find_by_username(username):
46
+ return jsonify({"error": "Username already taken"}), 409
47
+
48
+ # Create new user
49
+ user = User(username=username, email=email, password=password, preferences=preferences)
50
+ user_id = user.save()
51
+
52
+ # Generate JWT token
53
+ token = generate_jwt(user_id, username, email)
54
+
55
+ return jsonify({
56
+ "message": "User created successfully",
57
+ "token": token,
58
+ "user": {
59
+ "id": user_id,
60
+ "username": username,
61
+ "email": email,
62
+ "preferences": preferences
63
+ }
64
+ }), 201
65
+
66
+ except Exception as e:
67
+ print(f"Signup error: {e}")
68
+ return jsonify({"error": "Internal server error"}), 500
69
+
70
+ @auth_bp.route('/login', methods=['POST'])
71
+ def login():
72
+ try:
73
+ data = request.get_json()
74
+
75
+ if not data:
76
+ return jsonify({"error": "No data provided"}), 400
77
+
78
+ email = data.get('email', '').strip().lower()
79
+ password = data.get('password', '')
80
+
81
+ if not email or not password:
82
+ return jsonify({"error": "Email and password are required"}), 400
83
+
84
+ # Find user by email
85
+ user = User.find_by_email(email)
86
+ if not user or not user.check_password(password):
87
+ return jsonify({"error": "Invalid email or password"}), 401
88
+
89
+ # Update last login
90
+ user.update_last_login()
91
+
92
+ # Generate JWT token
93
+ token = generate_jwt(user._id, user.username, user.email)
94
+
95
+ return jsonify({
96
+ "message": "Login successful",
97
+ "token": token,
98
+ "user": {
99
+ "id": str(user._id),
100
+ "username": user.username,
101
+ "email": user.email,
102
+ "preferences": user.preferences,
103
+ "last_login": user.last_login.isoformat() if user.last_login else None
104
+ }
105
+ }), 200
106
+
107
+ except Exception as e:
108
+ print(f"Login error: {e}")
109
+ return jsonify({"error": "Internal server error"}), 500
routes/history.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from utils.jwt_helper import decode_jwt
3
+ from models.history import History
4
+
5
+ history_bp = Blueprint('history', __name__)
6
+
7
+ @history_bp.route('/history', methods=['GET'])
8
+ def get_history():
9
+ try:
10
+ # JWT Authentication
11
+ auth_header = request.headers.get('Authorization')
12
+ if not auth_header or not auth_header.startswith('Bearer '):
13
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
14
+
15
+ token = auth_header.split(" ")[1]
16
+ try:
17
+ user_data = decode_jwt(token)
18
+ user_id = user_data.get("user_id")
19
+ except Exception as e:
20
+ return jsonify({"error": "Invalid or expired token"}), 401
21
+
22
+ # Get query parameters
23
+ limit = min(int(request.args.get('limit', 20)), 100) # Max 100 items
24
+ offset = int(request.args.get('offset', 0))
25
+
26
+ # Get user history
27
+ history = History.get_user_history(user_id, limit=limit, offset=offset)
28
+
29
+ return jsonify({
30
+ "status": "success",
31
+ "history": history,
32
+ "count": len(history),
33
+ "limit": limit,
34
+ "offset": offset
35
+ }), 200
36
+
37
+ except Exception as e:
38
+ print(f"History error: {e}")
39
+ return jsonify({"error": "Internal server error"}), 500
40
+
41
+ @history_bp.route('/history/stats', methods=['GET'])
42
+ def get_history_stats():
43
+ try:
44
+ # JWT Authentication
45
+ auth_header = request.headers.get('Authorization')
46
+ if not auth_header or not auth_header.startswith('Bearer '):
47
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
48
+
49
+ token = auth_header.split(" ")[1]
50
+ try:
51
+ user_data = decode_jwt(token)
52
+ user_id = user_data.get("user_id")
53
+ except Exception as e:
54
+ return jsonify({"error": "Invalid or expired token"}), 401
55
+
56
+ # Get user stats
57
+ stats = History.get_user_stats(user_id)
58
+
59
+ return jsonify({
60
+ "status": "success",
61
+ "stats": stats
62
+ }), 200
63
+
64
+ except Exception as e:
65
+ print(f"Stats error: {e}")
66
+ return jsonify({"error": "Internal server error"}), 500
routes/recommend.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ import requests
3
+ import os
4
+ from dotenv import load_dotenv
5
+ from utils.jwt_helper import decode_jwt
6
+ from models.history import History
7
+
8
+ load_dotenv()
9
+
10
+ recommend_bp = Blueprint('recommend', __name__)
11
+
12
+ RECOMMENDER_ENDPOINTS = {
13
+ "movie": os.getenv("MOVIE_RECOMMENDER_URL"),
14
+ "book": os.getenv("BOOK_RECOMMENDER_URL"),
15
+ "tv": os.getenv("TV_RECOMMENDER_URL")
16
+ }
17
+
18
+ RESPONSE_KEYS = {
19
+ "movie": "movies",
20
+ "book": "books",
21
+ "tv": "shows"
22
+ }
23
+
24
+ @recommend_bp.route('/recommend/tvshowrec', methods=['POST'])
25
+ def recommend():
26
+ try:
27
+ data = request.get_json()
28
+
29
+ if not data:
30
+ return jsonify({"error": "No data provided"}), 400
31
+
32
+ rec_type = data.get('type')
33
+ genre = data.get('genre')
34
+ top_k = data.get('top_k', 10)
35
+
36
+ if not rec_type or not genre:
37
+ return jsonify({"error": "Missing 'type' or 'genre'"}), 400
38
+
39
+ if rec_type not in RECOMMENDER_ENDPOINTS or not RECOMMENDER_ENDPOINTS[rec_type]:
40
+ return jsonify({"error": "Invalid or missing recommender URL for type."}), 400
41
+
42
+ # JWT Authentication
43
+ auth_header = request.headers.get('Authorization')
44
+ if not auth_header or not auth_header.startswith('Bearer '):
45
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
46
+
47
+ token = auth_header.split(" ")[1]
48
+ try:
49
+ user_data = decode_jwt(token)
50
+ user_id = user_data.get("user_id")
51
+ except Exception as e:
52
+ return jsonify({"error": "Invalid or expired token"}), 401
53
+
54
+ # Process genres
55
+ genres_list = [g.strip() for g in genre.split(",")] if isinstance(genre, str) else genre
56
+
57
+ # Call microservice
58
+ try:
59
+ recommender_url = RECOMMENDER_ENDPOINTS[rec_type]
60
+ response = requests.post(
61
+ recommender_url,
62
+ json={"genres": genres_list},
63
+ timeout=10
64
+ )
65
+
66
+ if not response.ok:
67
+ return jsonify({
68
+ "error": f"{rec_type} service error",
69
+ "details": response.text,
70
+ "status_code": response.status_code
71
+ }), 500
72
+
73
+ result = response.json()
74
+ if result.get("status") != "success":
75
+ return jsonify({"error": result.get("message", "Unknown error")}), 500
76
+
77
+ raw_items = result.get(RESPONSE_KEYS.get(rec_type, "items"), [])
78
+
79
+ # Normalize response format
80
+ normalized = []
81
+ for item in raw_items:
82
+ normalized.append({
83
+ "type": rec_type,
84
+ "name": item.get("name") or item.get("title"),
85
+ "creator": item.get("director") or item.get("author") or item.get("creator"),
86
+ "description": item.get("description", ""),
87
+ "genre": item.get("genre", []),
88
+ "rating": item.get("rating"),
89
+ "year": item.get("year"),
90
+ "image_url": item.get("image_url")
91
+ })
92
+
93
+ # Save to history
94
+ try:
95
+ history = History(
96
+ user_id=user_id,
97
+ recommendation_type=rec_type,
98
+ genre=genres_list,
99
+ items=normalized,
100
+ query_params={"top_k": top_k}
101
+ )
102
+ history.save()
103
+ print(f"Saved recommendation history for user {user_id}")
104
+ except Exception as e:
105
+ print(f"Failed to save history: {e}")
106
+ # Don't fail the request if history saving fails
107
+
108
+ return jsonify({
109
+ "status": "success",
110
+ "recommendations": normalized,
111
+ "count": len(normalized),
112
+ "type": rec_type,
113
+ "genres": genres_list
114
+ }), 200
115
+
116
+ except requests.exceptions.Timeout:
117
+ return jsonify({"error": f"{rec_type} service timeout"}), 504
118
+ except requests.exceptions.RequestException as e:
119
+ return jsonify({"error": f"Failed to connect to {rec_type} service", "details": str(e)}), 503
120
+
121
+ except Exception as e:
122
+ print(f"Recommend error: {e}")
123
+ return jsonify({"error": "Internal server error"}), 500
124
+
125
+ @recommend_bp.route('/recommend/movies', methods=['POST'])
126
+ def recommend():
127
+ try:
128
+ data = request.get_json()
129
+
130
+ if not data:
131
+ return jsonify({"error": "No data provided"}), 400
132
+
133
+ rec_type = data.get('type')
134
+ genre = data.get('genre')
135
+ top_k = data.get('top_k', 10)
136
+
137
+ if not rec_type or not genre:
138
+ return jsonify({"error": "Missing 'type' or 'genre'"}), 400
139
+
140
+ if rec_type not in RECOMMENDER_ENDPOINTS or not RECOMMENDER_ENDPOINTS[rec_type]:
141
+ return jsonify({"error": "Invalid or missing recommender URL for type."}), 400
142
+
143
+ # JWT Authentication
144
+ auth_header = request.headers.get('Authorization')
145
+ if not auth_header or not auth_header.startswith('Bearer '):
146
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
147
+
148
+ token = auth_header.split(" ")[1]
149
+ try:
150
+ user_data = decode_jwt(token)
151
+ user_id = user_data.get("user_id")
152
+ except Exception as e:
153
+ return jsonify({"error": "Invalid or expired token"}), 401
154
+
155
+ # Process genres
156
+ genres_list = [g.strip() for g in genre.split(",")] if isinstance(genre, str) else genre
157
+
158
+ # Call microservice
159
+ try:
160
+ recommender_url = RECOMMENDER_ENDPOINTS[rec_type]
161
+ response = requests.post(
162
+ recommender_url,
163
+ json={"genres": genres_list, "top_k": top_k},
164
+ timeout=10
165
+ )
166
+
167
+ if not response.ok:
168
+ return jsonify({
169
+ "error": f"{rec_type} service error",
170
+ "details": response.text,
171
+ "status_code": response.status_code
172
+ }), 500
173
+
174
+ result = response.json()
175
+ if result.get("status") != "success":
176
+ return jsonify({"error": result.get("message", "Unknown error")}), 500
177
+
178
+ raw_items = result.get(RESPONSE_KEYS.get(rec_type, "items"), [])
179
+
180
+ # Normalize response format
181
+ normalized = []
182
+ for item in raw_items:
183
+ normalized.append({
184
+ "type": rec_type,
185
+ "name": item.get("name") or item.get("title"),
186
+ "creator": item.get("director") or item.get("author") or item.get("creator"),
187
+ "description": item.get("description", ""),
188
+ "genre": item.get("genre", []),
189
+ "rating": item.get("rating")
190
+ })
191
+
192
+ # Save to history
193
+ try:
194
+ history = History(
195
+ user_id=user_id,
196
+ recommendation_type=rec_type,
197
+ genre=genres_list,
198
+ items=normalized,
199
+ query_params={"top_k": top_k}
200
+ )
201
+ history.save()
202
+ print(f"Saved recommendation history for user {user_id}")
203
+ except Exception as e:
204
+ print(f"Failed to save history: {e}")
205
+ # Don't fail the request if history saving fails
206
+
207
+ return jsonify({
208
+ "status": "success",
209
+ "recommendations": normalized,
210
+ "count": len(normalized),
211
+ "type": rec_type,
212
+ "genres": genres_list
213
+ }), 200
214
+
215
+ except requests.exceptions.Timeout:
216
+ return jsonify({"error": f"{rec_type} service timeout"}), 504
217
+ except requests.exceptions.RequestException as e:
218
+ return jsonify({"error": f"Failed to connect to {rec_type} service", "details": str(e)}), 503
219
+
220
+ except Exception as e:
221
+ print(f"Recommend error: {e}")
222
+ return jsonify({"error": "Internal server error"}), 500
routes/user.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from utils.jwt_helper import decode_jwt
3
+ from models.user import User
4
+
5
+ user_bp = Blueprint('user', __name__)
6
+
7
+ @user_bp.route('/profile', methods=['GET'])
8
+ def get_profile():
9
+ try:
10
+ # JWT Authentication
11
+ auth_header = request.headers.get('Authorization')
12
+ if not auth_header or not auth_header.startswith('Bearer '):
13
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
14
+
15
+ token = auth_header.split(" ")[1]
16
+ try:
17
+ user_data = decode_jwt(token)
18
+ user_id = user_data.get("user_id")
19
+ except Exception as e:
20
+ return jsonify({"error": "Invalid or expired token"}), 401
21
+
22
+ # Get user profile
23
+ user = User.find_by_id(user_id)
24
+ if not user:
25
+ return jsonify({"error": "User not found"}), 404
26
+
27
+ return jsonify({
28
+ "status": "success",
29
+ "user": {
30
+ "id": str(user._id),
31
+ "username": user.username,
32
+ "email": user.email,
33
+ "preferences": user.preferences,
34
+ "created_at": user.created_at.isoformat(),
35
+ "last_login": user.last_login.isoformat() if user.last_login else None
36
+ }
37
+ }), 200
38
+
39
+ except Exception as e:
40
+ print(f"Profile error: {e}")
41
+ return jsonify({"error": "Internal server error"}), 500
42
+
43
+ @user_bp.route('/preferences', methods=['PUT'])
44
+ def update_preferences():
45
+ try:
46
+ # JWT Authentication
47
+ auth_header = request.headers.get('Authorization')
48
+ if not auth_header or not auth_header.startswith('Bearer '):
49
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
50
+
51
+ token = auth_header.split(" ")[1]
52
+ try:
53
+ user_data = decode_jwt(token)
54
+ user_id = user_data.get("user_id")
55
+ except Exception as e:
56
+ return jsonify({"error": "Invalid or expired token"}), 401
57
+
58
+ data = request.get_json()
59
+ if not data:
60
+ return jsonify({"error": "No data provided"}), 400
61
+
62
+ preferences = data.get('preferences')
63
+
64
+ if not preferences or not isinstance(preferences, dict):
65
+ return jsonify({"error": "Valid preferences object is required"}), 400
66
+
67
+ # Update user preferences
68
+ user = User.find_by_id(user_id)
69
+ if not user:
70
+ return jsonify({"error": "User not found"}), 404
71
+
72
+ user.update_preferences(preferences)
73
+
74
+ return jsonify({
75
+ "status": "success",
76
+ "message": "Preferences updated successfully",
77
+ "preferences": preferences
78
+ }), 200
79
+
80
+ except Exception as e:
81
+ print(f"Preferences error: {e}")
82
+ return jsonify({"error": "Internal server error"}), 500
utils/jwt_helper.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jwt
2
+ import os
3
+ from datetime import datetime, timedelta, timezone
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ SECRET_KEY = os.getenv("SECRET_KEY")
9
+
10
+ def generate_jwt(user_id, username, email):
11
+ payload = {
12
+ "user_id": str(user_id),
13
+ "username": username,
14
+ "email": email,
15
+ "exp": datetime.now(timezone.utc) + timedelta(days=7),
16
+ "iat": datetime.now(timezone.utc)
17
+ }
18
+ return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
19
+
20
+ def decode_jwt(token):
21
+ try:
22
+ payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
23
+ return payload
24
+ except jwt.ExpiredSignatureError:
25
+ raise Exception("Token has expired")
26
+ except jwt.InvalidTokenError:
27
+ raise Exception("Invalid token")