1
1
const express = require ( 'express' ) ;
2
2
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
+
3
8
4
9
// mongoDB models
5
10
const User = require ( './../models/User.jsx' ) ;
@@ -9,7 +14,6 @@ const Book = require('./../models/Book.jsx');
9
14
const CurrentlyReading = require ( './../models/CurrentlyReading.jsx' ) ;
10
15
11
16
12
-
13
17
// Password handler
14
18
const bcrypt = require ( 'bcrypt' ) ;
15
19
@@ -52,14 +56,16 @@ router.post('/createReview', (req, res) => {
52
56
53
57
// Posts a book to the DB (NOTE: DOES NOT HAVE DATA CHECKS)
54
58
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 ;
56
60
isbn = isbn ;
57
61
title = title . trim ( ) ;
58
62
author = author . trim ( ) ;
59
63
published = published . trim ( ) ;
60
64
description = description . trim ( ) ;
61
65
coverLink = coverLink . trim ( ) ;
62
66
67
+ genre = genre ? genre . trim ( ) : null ;
68
+
63
69
//Due to google books not having all info, just check that we have
64
70
//at least one metric to find the book by
65
71
if ( isbn == "" && title == "" && author == "" ) {
@@ -74,7 +80,8 @@ router.post('/createBook', (req, res) => {
74
80
author,
75
81
published,
76
82
description,
77
- coverLink
83
+ coverLink,
84
+ genre
78
85
} ) ;
79
86
80
87
newBook . save ( ) . then ( result => {
@@ -121,7 +128,7 @@ router.post('/createManualBook', (req, res) => {
121
128
author,
122
129
published,
123
130
description,
124
- coverLink
131
+ coverLink,
125
132
} ) ;
126
133
127
134
// Save the book to the database
@@ -952,42 +959,140 @@ router.post('/removeCurrentlyReading', async (req, res) => {
952
959
}
953
960
} ) ;
954
961
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 ) ;
958
967
959
968
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
+ } ) ;
961
984
}
962
985
963
986
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
+ }
965
994
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
+ } ) ;
968
1016
}
969
1017
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
+ }
985
1061
} ) ;
986
- } catch ( error ) {
987
1062
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
+ } ) ;
990
1093
}
991
1094
} ) ;
992
1095
993
- module . exports = router ;
1096
+
1097
+
1098
+ module . exports = router ;
0 commit comments