diff --git a/backend/app.js b/backend/app.js index 4578f18e..44361b8a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -37,7 +37,7 @@ const WebSocket = require('ws'); app.use( cors({ - origin: ["http://localhost:8081", "exp://192.168.225.19:8081", 'chrome-extension://gghoblbehihhmceogmigccldhmnnfeoc', "https://*.netflix.com/*"], + origin: ["http://localhost:8081", "exp://192.168.225.19:8081", 'chrome-extension://mbmbholeiljjaabgbpajlfalcbnciona', "https://www.netflix.com/*"], methods: ["GET", "POST", "PUT", "DELETE"], credentials: true, }) diff --git a/backend/src/Recommender/recommender.service.js b/backend/src/Recommender/recommender.service.js index 3beb3840..92348b90 100644 --- a/backend/src/Recommender/recommender.service.js +++ b/backend/src/Recommender/recommender.service.js @@ -19,11 +19,10 @@ const getUserFavoriteGenres = async (userId) => { const session = driver.session(); try { const result = await session.run( - 'MATCH (u:User {id: $userId}) RETURN u.favorite_genres AS favoriteGenres', + 'MATCH (u:User {uid: $userId}) RETURN u.favouriteGenres AS favoriteGenres', { userId } ); - // Assuming favorite_genres is stored as a list in Neo4j if (result.records.length > 0) { const favoriteGenres = result.records[0].get('favoriteGenres'); return favoriteGenres; // This should return an array of genre IDs @@ -38,26 +37,71 @@ const getUserFavoriteGenres = async (userId) => { } }; -// Don't forget to close the driver when your application is shutting down +// New function to get user interaction data (watched movies, liked movies, etc.) +const getUserInteractionData = async (userId) => { + const session = driver.session(); + try { + const result = await session.run( + 'MATCH (u:User {uid: $userId})-[:REVIEW]->(m:Movie) RETURN m.movieId AS movieId, m.rating AS rating', + { userId } + ); + + return result.records.map(record => ({ + movieId: record.get('movieId'), + rating: record.get('rating') || null + })); + } catch (error) { + console.error('Error fetching user interaction data:', error); + throw new Error('Failed to retrieve user interactions'); + } finally { + await session.close(); + } +}; + const closeNeo4jConnection = async () => { await driver.close(); }; const checkAndSetupIndex = async () => { - //await setupIndex(); - //await indexMovies(); // Ensure movies are indexed after setup + // await setupIndex(); + // await indexMovies(); // Ensure movies are indexed after setup +}; +const genresMap = { + 28: 'Action', + 12: 'Adventure', + 16: 'Animation', + 35: 'Comedy', + 80: 'Crime', + 99: 'Documentary', + 18: 'Drama', + 10751: 'Family', + 14: 'Fantasy', + 36: 'History', + 27: 'Horror', + 10402: 'Music', + 9648: 'Mystery', + 10749: 'Romance', + 878: 'Science Fiction', + 10770: 'TV Movie', + 53: 'Thriller', + 10752: 'War', + 37: 'Western' }; - +// Modified recommendation function to leverage collaborative filtering with similarity scaling const recommendMoviesByTMDBId = async (tmdbId, userId) => { try { await checkAndSetupIndex(); const tmdbMovie = await getMovieDetails(tmdbId); const userFavoriteGenres = await getUserFavoriteGenres(userId); + const userInteractions = await getUserInteractionData(userId); // Get watched/liked movies // Collect genre IDs for querying - const genreQuery = [...tmdbMovie.genres.map(genre => genre.id), ...userFavoriteGenres]; + const genreQuery = [ + ...tmdbMovie.genres.map(genre => genresMap[genre.id]), // Convert TMDB movie genres to names + ...userFavoriteGenres.map(genreId => genresMap[genreId]) // Convert favorite genre IDs to names + ].filter(Boolean); // Ensure no undefined values if an ID doesn't exist in the map console.log("Searching movies in Elasticsearch with genre query:", genreQuery); @@ -66,27 +110,17 @@ const recommendMoviesByTMDBId = async (tmdbId, userId) => { body: { query: { bool: { - filter: [ - { - range: { - 'popularity': { - 'gte': 10 // Popularity must be greater than 5 - } - } - } - ], should: [ { match: { overview: tmdbMovie.overview } }, - { terms: { 'genre_ids': genreQuery } } + { terms: { 'genre_names': genreQuery } } ], - must_not: [ { match: { id: tmdbId } } ], minimum_should_match: 1 } }, - size: 100 + size: 400 }, }); @@ -98,11 +132,7 @@ const recommendMoviesByTMDBId = async (tmdbId, userId) => { index: 'movies', body: { query: { - range: { - popularity: { - gt: 10 // Popularity must be greater than 5 - } - } + match_all: {} // Remove popularity condition and fetch all movies } }, size: 100 @@ -118,8 +148,6 @@ const recommendMoviesByTMDBId = async (tmdbId, userId) => { const allMoviesFeatures = allMovies.map(combineFeatures); const combinedFeatures = [tmdbMovieFeatures, ...allMoviesFeatures]; - console.log("Combined features for all movies: ", combinedFeatures); - // Vectorize the movie features const movieVectors = vectorizeMovies(combinedFeatures); const [tmdbMovieVector, ...allMoviesVectors] = movieVectors; @@ -134,17 +162,35 @@ const recommendMoviesByTMDBId = async (tmdbId, userId) => { }; }); + // Collaborative filtering: Give a boost to movies that the user has liked or watched + const enhancedSimilarities = cosineSimilarities.map(rec => { + const userInteraction = userInteractions.find(interaction => interaction.movieId === rec.movie.id); + if (userInteraction) { + rec.similarity += 0.1 * (userInteraction.rating || 1); // Boost based on rating + } + return rec; + }); + + // Normalize the similarity scores based on the number of movies to recommend + const numRecommendations = 15; + const maxSimilarity = Math.max(...enhancedSimilarities.map(rec => rec.similarity)); // Get max similarity for scaling + + const scaledSimilarities = enhancedSimilarities.map(rec => { + rec.similarity = (rec.similarity / maxSimilarity) * (1 / numRecommendations); // Scale similarity + return rec; + }); + // Sort the movies by similarity in descending order - cosineSimilarities.sort((a, b) => b.similarity - a.similarity); + scaledSimilarities.sort((a, b) => b.similarity - a.similarity); // Return the top 15 highest similarity movies - const topRecommendations = cosineSimilarities - .slice(0, 15) + const topRecommendations = scaledSimilarities + .slice(0, numRecommendations) .map(rec => ({ id: rec.movie.id, title: rec.movie.title, posterUrl: `https://image.tmdb.org/t/p/w500${rec.movie.poster_path}`, - similarity: (rec.similarity * 100).toFixed(2) + similarity: (((rec.similarity * 100).toFixed(2)*10).toFixed(2)) // Adjust similarity scaling for display })); console.log("Top recommendations: ", topRecommendations); @@ -161,11 +207,10 @@ const combineFeatures = (movie) => { const overview = movie.overview || ''; const title = movie.title || ''; const director = movie.director || ''; - const cast = Array.isArray(movie.cast) ? movie.cast.join(' ') : ''; - const music = movie.music || ''; + // const cast = Array.isArray(movie.cast) ? movie.cast.join(' ') : ''; + // const music = movie.music || ''; - // Combine all available features into a single string - return `${overview} ${genres} ${title} ${director} ${cast} ${music}`.trim(); + return `${overview} ${genres} ${title} ${director}`.trim(); }; module.exports = { recommendMoviesByTMDBId }; diff --git a/backend/src/Room/WatchParty/party.controller.js b/backend/src/Room/WatchParty/party.controller.js index 3f67713b..ddef567b 100644 --- a/backend/src/Room/WatchParty/party.controller.js +++ b/backend/src/Room/WatchParty/party.controller.js @@ -3,7 +3,7 @@ const partyService = require('./party.service'); // Controller for starting a watch party exports.startWatchParty = async (req, res) => { console.log('Starting watch party -> controller'); - res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://gghoblbehihhmceogmigccldhmnnfeoc'); + res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://mbmbholeiljjaabgbpajlfalcbnciona'); const { username, roomShortCode, partyCode } = req.body; if (!partyCode || !roomShortCode || !username) { @@ -27,7 +27,7 @@ exports.startWatchParty = async (req, res) => { // Controller for joining a watch party exports.joinWatchParty = async (req, res) => { const { username, partyCode } = req.body; - res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://gghoblbehihhmceogmigccldhmnnfeoc'); + res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://mbmbholeiljjaabgbpajlfalcbnciona'); if (!username || !partyCode) { return res.status(400).json({ success: false, message: 'Username and Party Code are required' }); } @@ -54,25 +54,38 @@ exports.joinWatchParty = async (req, res) => { // Fetch chat messages for a watch party exports.getWatchPartyChatMessages = async (req, res) => { console.log('Get Watch Party-controller'); - res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://gghoblbehihhmceogmigccldhmnnfeoc'); + res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://mbmbholeiljjaabgbpajlfalcbnciona'); + res.setHeader('Access-Control-Allow-Origin', 'https://www.netflix.com'); + const { partyCode } = req.params; console.log("Controller party Code-> ", partyCode); + try { const { roomId } = await partyService.getWatchPartyByCode(partyCode); // Ensure this function exists in the service const messages = await partyService.getWatchPartyMessages(roomId); + if (messages.success) { - res.status(200).json(messages); + if (messages.messages.length === 0) { + console.log("LOOK?"); + return res.status(200).json({ success: true, message: "No messages available in the chat room." }); + } + return res.status(200).json(messages); // Return the messages if available + } else { + return res.status(500).json({ success: false, error: 'Failed to fetch chat messages' }); } } catch (error) { console.error('Error fetching chat messages:', error); - res.status(500).json({ error: 'Failed to fetch chat messages' }); + res.status(500).json({ success: false, error: 'Failed to fetch chat messages' }); } }; + // Send a chat message to a watch party exports.sendWatchPartyChatMessage = async (req, res) => { - res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://gghoblbehihhmceogmigccldhmnnfeoc'); + res.setHeader('Access-Control-Allow-Origin', 'chrome-extension://mbmbholeiljjaabgbpajlfalcbnciona'); + res.setHeader('Access-Control-Allow-Origin', 'https://www.netflix.com'); + const { partyCode } = req.params; const { username, text, id: messageId } = req.body; // Include messageId diff --git a/backend/src/utils/vectorizeMovies.js b/backend/src/utils/vectorizeMovies.js index 45880d68..7ab093f2 100644 --- a/backend/src/utils/vectorizeMovies.js +++ b/backend/src/utils/vectorizeMovies.js @@ -12,22 +12,22 @@ function enhancedTokenize(text, n = 1) { return n === 1 ? filteredTokens : natural.NGrams.ngrams(filteredTokens, n).map(ngram => ngram.join(' ')); } -function vectorizeMovies(combinedFeatures) { +function vectorizeMovies(combinedFeatures, n = 1) { const tfidf = new TfIdf(); const termSet = new Set(); // First pass: Add each movie's combined features to the TfIdf instance combinedFeatures.forEach(features => { if (features.trim().length > 0) { // Check for non-empty features - const tokens = enhancedTokenize(features, 1); // Use unigrams + const tokens = enhancedTokenize(features, n); // Use n-grams (can be unigrams, bigrams, etc.) tfidf.addDocument(tokens.join(' ')); // Rejoin tokens for TfIdf tokens.forEach(term => { - termSet.add(term); + termSet.add(term); // Collect terms for vocabulary }); } }); - const vocabulary = Array.from(termSet); + const vocabulary = Array.from(termSet); // Create the final vocabulary array const movieVectors = combinedFeatures.map((features, index) => { const vector = new Array(vocabulary.length).fill(0); const terms = tfidf.listTerms(index); diff --git a/frontend/Chrome Extension/js/content.js b/frontend/Chrome Extension/js/content.js index cd677de6..dcf3ce10 100644 --- a/frontend/Chrome Extension/js/content.js +++ b/frontend/Chrome Extension/js/content.js @@ -997,8 +997,16 @@ async function sendMessage(partyCode, username, text) { function addChatMessage(username, text, isUser, timestamp) { const chatMessage = document.createElement("p"); - // Format the timestamp - const formattedTime = new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + // Check if the timestamp is valid + const messageTimestamp = new Date(timestamp); + const isValidDate = !isNaN(messageTimestamp.getTime()); // Checks if the timestamp is valid + + // If the timestamp is not valid, use the current time + const finalTimestamp = isValidDate ? messageTimestamp : new Date(); + + // Format the timestamp as HH:MM + const formattedTime = finalTimestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + chatMessage.textContent = `[${username}] ${text} - ${formattedTime}`; // Append the timestamp to the message chatMessage.className = "chat-message"; // Assign the base chat-message class @@ -1071,10 +1079,10 @@ async function initChat() { } }); // Set an interval to fetch chat messages every 5 seconds - setInterval(() => { - fetchChatMessages(userDetails.username, userDetails.partyCode); - //createChatInputContainer(userDetails); - }, 5000); // Adjust interval as necessary (5000ms = 5 seconds) + // setInterval(() => { + // fetchChatMessages(userDetails.username, userDetails.partyCode); + // //createChatInputContainer(userDetails); + // }, 5000); // Adjust interval as necessary (5000ms = 5 seconds) } catch (error) { console.error('Initialization error:', error); }