Skip to content

Commit dc94bf4

Browse files
Merge pull request #6 from Aim-ez/version_two
Plot-Line Recommendations
2 parents db4c985 + 62378c7 commit dc94bf4

File tree

11 files changed

+616
-60
lines changed

11 files changed

+616
-60
lines changed

login_server/.env

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
MONGODB_URI=mongodb+srv://theaimeeschmidt:[email protected]/UserDB?retryWrites=true&w=majority&appName=Cluster0
1+
MONGODB_URI=mongodb+srv://theaimeeschmidt:[email protected]/UserDB?retryWrites=true&w=majority&appName=Cluster0
2+
GOOGLE_BOOKS_API_KEY=AIzaSyA4Z1Qm7N2_6AnPLHOtS577y4-nV_NrAb8
3+
GOOGLE_BOOKS_API_BASE_URL=https://www.googleapis.com/books/v1/volumes

login_server/api/User.jsx

Lines changed: 135 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
const express = require('express');
22
const router = express.Router();
3+
const axios = require('axios');
4+
5+
const GOOGLE_BOOKS_API_KEY = 'AIzaSyA4Z1Qm7N2_6AnPLHOtS577y4-nV_NrAb8';
6+
const GOOGLE_BOOKS_API = 'https://www.googleapis.com/books/v1/volumes';
7+
38

49
// mongoDB models
510
const User = require('./../models/User.jsx');
@@ -9,7 +14,6 @@ const Book = require('./../models/Book.jsx');
914
const CurrentlyReading = require('./../models/CurrentlyReading.jsx');
1015

1116

12-
1317
// Password handler
1418
const bcrypt = require('bcrypt');
1519

@@ -52,14 +56,16 @@ router.post('/createReview', (req, res) => {
5256

5357
// Posts a book to the DB (NOTE: DOES NOT HAVE DATA CHECKS)
5458
router.post('/createBook', (req, res) => {
55-
let {isbn, title, author, published, description, coverLink} = req.body;
59+
let {isbn, title, author, published, description, coverLink, genre} = req.body;
5660
isbn = isbn;
5761
title = title.trim();
5862
author = author.trim();
5963
published = published.trim();
6064
description = description.trim();
6165
coverLink = coverLink.trim();
6266

67+
genre = genre ? genre.trim() : null;
68+
6369
//Due to google books not having all info, just check that we have
6470
//at least one metric to find the book by
6571
if (isbn == "" && title == "" && author=="") {
@@ -74,7 +80,8 @@ router.post('/createBook', (req, res) => {
7480
author,
7581
published,
7682
description,
77-
coverLink
83+
coverLink,
84+
genre
7885
});
7986

8087
newBook.save().then(result => {
@@ -121,7 +128,7 @@ router.post('/createManualBook', (req, res) => {
121128
author,
122129
published,
123130
description,
124-
coverLink
131+
coverLink,
125132
});
126133

127134
// Save the book to the database
@@ -952,42 +959,140 @@ router.post('/removeCurrentlyReading', async (req, res) => {
952959
}
953960
});
954961

955-
// Fetch user's currently reading list
956-
router.get('/getCurrentlyReading', async (req, res) => {
957-
const { userId } = req.query;
962+
963+
router.get('/recommendations', async (req, res) => {
964+
const { userId, selectedLanguage, sortOrder, resultsPerPage = 30 } = req.query;
965+
966+
console.log("Received request for recommendations:", userId);
958967

959968
if (!userId) {
960-
return res.status(400).json({ status: 'FAILED', message: 'Missing userId parameter.' });
969+
return res.status(400).json({
970+
status: "FAILED",
971+
message: "User ID is required.",
972+
});
973+
}
974+
975+
const GOOGLE_BOOKS_API_BASE_URL = process.env.GOOGLE_BOOKS_API_BASE_URL;
976+
const GOOGLE_BOOKS_API_KEY = process.env.GOOGLE_BOOKS_API_KEY;
977+
978+
if (!GOOGLE_BOOKS_API_BASE_URL || !GOOGLE_BOOKS_API_KEY) {
979+
console.error("Google Books API configuration is missing.");
980+
return res.status(500).json({
981+
status: "FAILED",
982+
message: "Internal server error. API configuration is missing.",
983+
});
961984
}
962985

963986
try {
964-
const currentlyReading = await CurrentlyReading.findOne({ userId }).populate('books.book');
987+
const user = await User.findById(userId);
988+
if (!user) {
989+
return res.status(400).json({
990+
status: "FAILED",
991+
message: "User not found.",
992+
});
993+
}
965994

966-
if (!currentlyReading || currentlyReading.books.length === 0) {
967-
return res.status(404).json({ status: 'SUCCESS', message: 'No books in the currently reading list.', data: { books: [] } });
995+
const readingList = await ReadingList.findOne({ userId }).populate('books');
996+
const reviews = await Review.find({ userId });
997+
998+
const preferences = {
999+
authors: {},
1000+
titles: {},
1001+
genres: {}
1002+
};
1003+
1004+
const addPreference = (category, item, weight) => {
1005+
if (!item) return;
1006+
preferences[category][item] = (preferences[category][item] || 0) + weight;
1007+
};
1008+
1009+
// Collect weighted preferences (Removed favorite book influence)
1010+
if (readingList?.books) {
1011+
readingList.books.forEach((book) => {
1012+
addPreference('titles', book.title, 3);
1013+
addPreference('authors', book.author, 3);
1014+
addPreference('genres', book.genre, 3);
1015+
});
9681016
}
9691017

970-
res.status(200).json({
971-
status: 'SUCCESS',
972-
message: 'Currently reading list retrieved successfully.',
973-
data: {
974-
books: currentlyReading.books.map((entry) => ({
975-
book: {
976-
_id: entry.book._id,
977-
title: entry.book.title,
978-
author: entry.book.author,
979-
coverLink: entry.book.coverLink,
980-
description: entry.book.description,
981-
},
982-
status: entry.status,
983-
})),
984-
},
1018+
if (reviews.length > 0) {
1019+
reviews.forEach((review) => {
1020+
const weight = review.rating || 1; // If no rating, default to 1
1021+
addPreference('titles', review.bookTitle, weight); // Influence titles with review weight
1022+
addPreference('authors', review.bookAuthor, weight); // Influence authors with review weight
1023+
addPreference('genres', review.bookGenre, weight); // Influence genres with review weight
1024+
});
1025+
}
1026+
1027+
// Sort preferences by weight
1028+
const sortedAuthors = Object.entries(preferences.authors).sort((a, b) => b[1] - a[1]);
1029+
const sortedGenres = Object.entries(preferences.genres).sort((a, b) => b[1] - a[1]);
1030+
1031+
const mostFrequentAuthor = sortedAuthors[0]?.[0] || null;
1032+
const mostFrequentGenre = sortedGenres[0]?.[0] || null;
1033+
1034+
// Fetch books
1035+
const authorQuery = mostFrequentAuthor ? `inauthor:${mostFrequentAuthor}` : '';
1036+
const genreQuery = mostFrequentGenre ? `subject:${mostFrequentGenre}` : '';
1037+
1038+
// Fetch books using Google Books API
1039+
const fetchBooks = async (query, startIndex = 0) => {
1040+
if (!query) return [];
1041+
const encodedQuery = encodeURIComponent(query);
1042+
const url = `${GOOGLE_BOOKS_API_BASE_URL}?q=${encodedQuery}&key=${GOOGLE_BOOKS_API_KEY}&maxResults=30&startIndex=${startIndex}`;
1043+
const response = await axios.get(url);
1044+
return response.data.items || [];
1045+
};
1046+
1047+
// Fetch books for both author and genre with an offset for the genre
1048+
const [authorBooks, genreBooks, randomBooks] = await Promise.all([
1049+
fetchBooks(authorQuery),
1050+
fetchBooks(genreQuery, 30), // Offset genre books by 30 to reduce overlap
1051+
fetchBooks("subject:fiction") // Add random genre books for variety (you can change this)
1052+
]);
1053+
1054+
// Combine results and deduplicate using book ID
1055+
const bookSet = new Map();
1056+
[...authorBooks, ...genreBooks, ...randomBooks].forEach((item) => {
1057+
const bookId = item.id || item.volumeInfo?.title; // Use ID if available, fallback to title
1058+
if (bookId && !bookSet.has(bookId)) {
1059+
bookSet.set(bookId, item);
1060+
}
9851061
});
986-
} catch (error) {
9871062

988-
console.error(error);
989-
res.status(500).json({ status: 'FAILED', message: 'Server error.' });
1063+
// Sort books based on the weight of preferences
1064+
const sortedRecommendations = Array.from(bookSet.values()).map((item) => ({
1065+
id: item.id,
1066+
title: item.volumeInfo?.title || "Untitled",
1067+
authors: item.volumeInfo?.authors || ["Unknown Author"],
1068+
genre: item.volumeInfo?.categories?.[0] || "Unknown Genre",
1069+
description: item.volumeInfo?.description || "No description available.",
1070+
coverLink: item.volumeInfo?.imageLinks?.thumbnail || "default_cover_url",
1071+
weight: (preferences.titles[item.volumeInfo?.title] || 0) +
1072+
(item.volumeInfo?.authors?.some((author) => preferences.authors[author]) ? 1 : 0) +
1073+
(preferences.genres[item.volumeInfo?.categories?.[0]] || 0)
1074+
}));
1075+
1076+
// Sort recommendations by weight first, then introduce randomness
1077+
const sortedAndRandomized = sortedRecommendations.sort((a, b) => b.weight - a.weight).slice(0, resultsPerPage);
1078+
1079+
// Introduce a shuffle to increase variety (randomize the order)
1080+
const shuffledRecommendations = sortedAndRandomized.sort(() => Math.random() - 0.5);
1081+
1082+
// Send the response
1083+
return res.status(200).json({
1084+
status: "SUCCESS",
1085+
data: shuffledRecommendations,
1086+
});
1087+
} catch (error) {
1088+
console.error("Error fetching recommendations:", error.message);
1089+
return res.status(500).json({
1090+
status: "FAILED",
1091+
message: "An error occurred while fetching recommendations.",
1092+
});
9901093
}
9911094
});
9921095

993-
module.exports = router;
1096+
1097+
1098+
module.exports = router;

login_server/models/Book.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const BookSchema = new Schema({
88
published: Date,
99
description: String,
1010
coverLink: String,
11+
genre: { type: String, required: false },
1112
})
1213

1314
const Book = mongoose.model('Book', BookSchema);

login_server/node_modules/.package-lock.json

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.

0 commit comments

Comments
 (0)