import { catchAsyncAction, generateToken, makeResponse, } from '../../helper/index.js'; import { responseMessages, statusCodes, } from '../../helper/makeResponse/index.js'; import { countUsers, countUserUploadsByUserId, createUpload, createUser, getUploadVideoById, getUserByEmail, getUserByFilter, getUserById, getUserUpload, getUserUploadByDateFilter, updateUserProfile, } from '../../services/users.js'; import bcrypt from 'bcrypt'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { privateKey } from '../../config/privateKeys.js'; import axios from 'axios'; import upload from '../../middlewares/upload.js'; import moment from 'moment'; import { Op } from 'sequelize'; import { getUserAnswersByUserId } from '../../services/assessment.js'; // Get the current module directory const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Handles user login by validating credentials and generating a JWT token. * * @param {Object} req - The request object containing user credentials. * @param {Object} res - The response object used to send responses. * @throws {Error} - If an error occurs during the login process. * @returns {Object} - The response object with user information and tokens. */ export const userLogin = catchAsyncAction(async (req, res) => { const { email, password, rememberMe } = req.body; // Validate required fields if (!email || !password) { return makeResponse( res, statusCodes.BAD_REQUEST, false, 'Email and password are required', {} ); } try { // Check if the user exists const user = await getUserByEmail(email); if (!user) { return makeResponse( res, statusCodes.BAD_REQUEST, false, responseMessages.USER_NOT_FOUND, {} ); } const updatedUser = await updateUserProfile(user.id, { rememberMe }); // Compare the provided password with the hashed password in the database const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return makeResponse( res, statusCodes.UNAUTHORIZED, false, responseMessages.INVALID_CREDENTIALS, {} ); } const QuizPlayed = await getUserAnswersByUserId(user.id); // If login is successful, generate a JWT token const payload = { userId: user.id, email: user.email, username: user.username, fullname: user.full_name, }; // Generate a JWT token using the payload and rememberMe flag const token = await generateToken(payload, rememberMe); // JWT secret stored in .env // Prepare the response data including user information and tokens const responseData = { user: { id: user.id, username: user.username, email: user.email, phoneNumber: user.phone_number, isQuizPlayed: QuizPlayed.length > 0 ? true : false, // Check if the user has played a quiz profilePic: user.profile_pic ? privateKey.NODE_SERVER_URL + user?.profile_pic : "", // User profile picture URL rememberMe }, token: token.accessToken, // Access token for authentication refreshToken: token.refreshToken // Refresh token for obtaining new access tokens }; // Send the response with a success status and the response data return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.LOGIN_SUCCESS, responseData ); } catch (error) { // Handle errors that may occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message // Send an error response return makeResponse(res, code, false, message, {}); } }); /** * Handles user signup by validating input, checking for existing users, * hashing the password, and creating a new user record in the database. * * @param {Object} req - The request object containing user signup data. * @param {Object} res - The response object used to send responses back to the client. * @returns {Object} - A response object indicating the success or failure of the signup process. * @throws {Error} - If an error occurs during the signup process, an appropriate error response is returned. */ export const userSignUp = catchAsyncAction(async (req, res) => { // Destructure user details from the request body const { username, email, password, phoneNumber, fullName } = req.body; // Check if the environment is development and limit the number of signups if (privateKey.NODE_ENV === 'development') { const usercount = await countUsers(); // If the user count exceeds 30, return an error response if (usercount >= 30) { return makeResponse( res, statusCodes.BAD_REQUEST, false, 'Max 8 signup limit reached', {} ); } } // Validate required fields and return an error if any are missing if (!username || !email || !password || !phoneNumber || !fullName) { return makeResponse( res, statusCodes.BAD_REQUEST, false, 'Missing required fields', {} ); } try { // Check if the email already exists in the database const emailCheckResult = await getUserByEmail(email); // If the email is found, return an error response if (emailCheckResult) { return makeResponse( res, statusCodes.BAD_REQUEST, false, responseMessages.USER_ALREADY_EXISTS, {} ); } // Check if the username already exists const usernameExists = await getUserByFilter({ username }); // If the username is found, return an error response if (usernameExists) { return makeResponse( res, statusCodes.BAD_REQUEST, false, responseMessages.USERNAME_ALREADY_EXISTS, {} ); } // Hash the password using bcrypt with a specified number of salt rounds const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); // Create a new user record in the database const result = await createUser({ username, email, password: hashedPassword, phone_number: phoneNumber, full_name: fullName }); // Send back the newly created user data, excluding the password const user = result.dataValues; // Delete the password field from the user object delete user.password; return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.USER_CREATED, user ); } catch (error) { const code = error?.code || statusCodes.SERVER_ERROR; console.log(error); const message = error?.message || responseMessages.SOMETHING_WRONG; return makeResponse(res, code, false, message, {}); } }); /** * Handles the user profile update process by validating input, * checking for existing usernames and emails, and updating user details * in the database. It also manages password hashing and profile picture uploads. * * @param {Object} req - The request object containing user profile data. * @param {Object} res - The response object used to send responses back to the client. * @returns {Object} - A response object indicating the success or failure of the update process. * @throws {Error} - If an error occurs during the update process, an appropriate error response is returned. */ export const updateUserProfileHandler = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT const { username, fullName, phoneNumber, password, email, age, gender, location } = req.body; try { // Fetch current user details to check for changes const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Prepare fields to be updated, and only update those fields that are provided in the request const updatedFields = {}; // Check if a new username is provided and validate its uniqueness if (username) { const usernameExists = await getUserByFilter({ username, id: { [Op.ne]: userId } }); // Ensures id is not equal to userId if (usernameExists) { return makeResponse( res, statusCodes.BAD_REQUEST, false, responseMessages.USERNAME_ALREADY_EXISTS, {} ); } updatedFields.username = username; // Add username to updatedFields } // Check if a new email is provided and validate its uniqueness if (email) { const userExists = await getUserByFilter({ email, id: { [Op.ne]: userId } }); if (userExists) { return makeResponse( res, statusCodes.BAD_REQUEST, false, responseMessages.USER_ALREADY_EXISTS, {} ); } updatedFields.email = email; // Add email to updatedFields } // Update other fields if provided if (phoneNumber) updatedFields.phone_number = phoneNumber; if (fullName) updatedFields.full_name = fullName; if (age) updatedFields.age = age; if (gender) updatedFields.gender = gender.toUpperCase(); if (location) updatedFields.location = location.toUpperCase(); // If a new password is provided, hash it and add it to the updatedFields object if (password) { const saltRounds = 10; // Define salt rounds for hashing updatedFields.password = await bcrypt.hash(password, saltRounds); // Hash the password } // If a new profile picture is uploaded, update the profile_pic field if (req.files && req.files.profilePic) { const profilePic = req.files.profilePic[0]; // Access the uploaded file const profilePicPath = path.join('public', 'images', profilePic.filename); // Path to save in DB // Delete the old profile picture if it exists if (currentUser.profile_pic) { // Extract the filename from the database value (assuming profile_pic has the full path in DB) const oldProfilePicFilename = path.basename(currentUser.profile_pic); // Construct the full path to the old profile pic const oldProfilePicPath = path.join( __dirname, '../../..', 'public', 'images', oldProfilePicFilename ); // Debugging: Print the full path being checked console.log('Attempting to delete file at path:', oldProfilePicPath); // Check if the old profile pic exists before attempting to delete fs.exists(oldProfilePicPath, (exists) => { if (exists) { console.log('Old profile picture exists, attempting to delete...'); fs.unlink(oldProfilePicPath, (err) => { if (err) { console.log('Error deleting old profile pic', err); } else { console.log('Old profile picture deleted successfully'); } }); } else { console.log( 'Old profile picture does not exist at path:', oldProfilePicPath ); } }); } updatedFields.profile_pic = profilePicPath; // Save the new profile picture path in the DB } // If no fields to update, return a message if (Object.keys(updatedFields).length === 0) { return makeResponse( res, statusCodes.BAD_REQUEST, false, 'No fields to update', // Message indicating no updates {} ); } // Update user profile with only the provided fields const updatedUser = await updateUserProfile(userId, updatedFields); // Ensure the password is not included in the response delete updatedUser.dataValues.password; // Construct the full URL for the profile picture if it exists updatedUser.profile_pic = updatedUser?.dataValues?.profile_pic ? privateKey.NODE_SERVER_URL + updatedUser?.dataValues?.profile_pic : ""; // Return the updated user data (excluding password) return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.USER_UPDATED, updatedUser // Return updated user data ); } catch (error) { // Handle any errors that occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message return makeResponse(res, code, false, message, {}); // Return error response } }); // Helper function to handle AI analysis const getAIAnalysis = async (videoFilePath, type) => { const postApiUrl = `${privateKey.AI_SERVER_URL}upload?video_url=${privateKey.NODE_SERVER_URL}${videoFilePath}&type=${type}`; console.log(postApiUrl); try { const aiUploadResponse = await axios.post(postApiUrl); if (!aiUploadResponse.data.analysis_id) return null; const analysisData = { overall_metrics: JSON.stringify(aiUploadResponse.data), total_duration: aiUploadResponse.data.analysis_json.total_duration, shots: {}, }; // Fetch shot-specific analysis for (const shot of aiUploadResponse.data.analysis_json.shots) { analysisData.shots[shot.classification] = JSON.stringify(shot); } return analysisData; } catch (err) { console.error('AI analysis error:', err); return null; } }; const getAIManualAnalysis = async (data) => { // Construct the API URL for manual analysis const postApiUrl = `${privateKey.AI_SERVER_URL}manual_analysis`; console.log(postApiUrl); try { // Send a POST request to the AI server with the provided data const aiUploadResponse = await axios.post(postApiUrl, data); // Check if the response contains an analysis ID if (!aiUploadResponse.data.analysis_id) return null; // Prepare the analysis data structure const analysisData = { overall_metrics: JSON.stringify(aiUploadResponse.data), // Store overall metrics as a JSON string total_duration: aiUploadResponse.data.analysis_json.total_duration, // Extract total duration shots: {}, // Initialize shots object }; // Fetch shot-specific analysis for (const shot of aiUploadResponse.data.analysis_json.shots) { analysisData.shots[shot.classification] = JSON.stringify(shot); // Store each shot's analysis } return analysisData; // Return the prepared analysis data } catch (err) { console.error('AI manual analysis error:', err); // Log any errors that occur return null; // Return null in case of an error } }; /** * Handles the upload of a user's recorded tennis video, processes AI analysis on the video, * and updates the user's video record in the database. * * @param {Object} req - The request object containing user video upload data. * @param {Object} res - The response object used to send responses back to the client. * @returns {Object} - A response object indicating the success or failure of the video upload process. * @throws {Error} - If an error occurs during the video upload or processing, an appropriate error response is returned. */ export const updateUserRecorderVideo = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT try { // Fetch the current user details based on userId const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Get today's date using moment.js const today = moment(); // Get the day of the week const dayOfWeek = today.format('dddd'); // Prepare fields to be updated in the database const updatedFields = { user_id: userId, file_name: '', file_path: '', file_type: '', upload_day: dayOfWeek, overall_metrics: '', backhand: '', forehand: '', service: '', smash: '', total_duration: '', }; // Handle file upload (video) if (req.files && req.files.userVideo) { const uploadedFile = req.files.userVideo[0]; // Access the uploaded video file const uploadedFilepath = path.join( 'public', 'videos', uploadedFile.filename // Path to save the uploaded video ); // Update fields with the uploaded file details updatedFields.file_name = uploadedFile.filename; updatedFields.file_path = uploadedFilepath; updatedFields.file_type = uploadedFile.mimetype; } // If a video file is uploaded, process AI analysis if (updatedFields.file_path) { const aiAnalysisData = await getAIAnalysis(updatedFields.file_path, 1); // Perform AI analysis on the video // If AI analysis data is returned, update the relevant fields if (aiAnalysisData) { updatedFields.overall_metrics = aiAnalysisData.overall_metrics; updatedFields.total_duration = aiAnalysisData.total_duration; Object.assign(updatedFields, aiAnalysisData.shots); // Merge shot data into updatedFields } } console.log(updatedFields); // Debugging: Log the updated fields // Validate that overall metrics have been provided if (updatedFields?.overall_metrics === '') { const code = statusCodes.BAD_REQUEST; const message = "Please upload the tennis specific video."; // Message indicating the need for a valid video return makeResponse(res, code, false, message, {}); } // Save the updated data in the database const data = await createUpload(updatedFields); // Prepare the response data to return to the client const updatedData = { id: data.id, user_id: data.user_id, file_path: data.file_path ? privateKey.NODE_SERVER_URL + data.file_path // Construct full URL for the file path : '', total_duration: data.total_duration, upload_time: data.upload_time, upload_day: data.upload_day, }; // Return a success response with the updated data return makeResponse( res, statusCodes.SUCCESS, true, 'Video uploaded successfully', updatedData ); } catch (error) { const code = error?.code || statusCodes.SERVER_ERROR; const message = error?.message || responseMessages.SOMETHING_WRONG; return makeResponse(res, code, false, message, {}); } }); export const updateUserRecorderMiniVideo = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT try { const currentUser = await getUserById(userId); if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Get today's date in the format dd-mm-yyyy const today = moment(); const dayOfWeek = today.format('dddd'); const updatedFields = { user_id: userId, file_name: '', file_path: '', file_type: '', upload_day: dayOfWeek, overall_metrics: '', backhand: '', forehand: '', service: '', smash: '', total_duration: '', }; // Handle file upload (video) if (req.files && req.files.userVideo) { const uploadedFile = req.files.userVideo[0]; const uploadedFilepath = path.join( 'public', 'videos', uploadedFile.filename ); updatedFields.file_name = uploadedFile.filename; updatedFields.file_path = uploadedFilepath; updatedFields.file_type = uploadedFile.mimetype; } // If video file is uploaded, process AI analysis if (updatedFields.file_path) { const aiAnalysisData = await getAIAnalysis(updatedFields.file_path, 0); if (aiAnalysisData) { updatedFields.overall_metrics = aiAnalysisData.overall_metrics; updatedFields.total_duration = aiAnalysisData.total_duration; Object.assign(updatedFields, aiAnalysisData.shots); } } if (updatedFields?.overall_metrics === '') { const code = statusCodes.BAD_REQUEST; const message = "Please upload the tennis specific video."; return makeResponse(res, code, false, message, {}); } // Save the updated data const data = await createUpload(updatedFields); const updatedData = { id: data.id, user_id: data.user_id, file_path: data.file_path ? privateKey.NODE_SERVER_URL + data.file_path : '', total_duration: data.total_duration, upload_time: data.upload_time, upload_day: data.upload_day, overall_metrics: data.overall_metrics !== '' ? JSON.parse(data.overall_metrics) : {} }; // data.backhand = data.backhand !== '' ? JSON.parse(data.backhand) : {}; // data.forehand = data.forehand !== '' ? JSON.parse(data.forehand) : {}; // data.service = data.service !== '' ? JSON.parse(data.service) : {}; // data.smash = data.smash !== '' ? JSON.parse(data.smash) : {}; return makeResponse( res, statusCodes.SUCCESS, true, 'Video uploaded successfully', updatedData ); } catch (error) { const code = error?.code || statusCodes.SERVER_ERROR; const message = error?.message || responseMessages.SOMETHING_WRONG; return makeResponse(res, code, false, message, {}); } }); export const uploadManualEntry = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT // Validate required parameters in the request body if (!req?.body?.accuracy || !req?.body?.shot || !req?.body?.consistency || !req?.body?.speed || !req?.body?.spinRateRight || !req?.body?.spinRateLeft || !req?.body?.duration) { return makeResponse(res, statusCodes.BAD_REQUEST, 'Invalid Parameters', {}); // Return a BAD_REQUEST response if parameters are missing } try { // Fetch the current user details to ensure they exist const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Get today's date using moment.js const today = moment(); // Get the day of the week (e.g., Monday, Tuesday) const dayOfWeek = today.format('dddd'); // Prepare an object to hold the updated fields for the upload const updatedFields = { user_id: userId, file_name: '', file_path: '', file_type: '', upload_day: dayOfWeek, overall_metrics: '', backhand: '', forehand: '', service: '', smash: '', // Placeholder for smash data total_duration: '', // Placeholder for total duration }; // Prepare the manual data object for analysis const manualData = { "analysis_id": `${Date.now()}`, // Unique analysis ID based on current timestamp "analysis_json": { "fps": req?.body?.fps ?? null, // Frames per second, default to null if not provided "shots": [ { "accuracy": req?.body?.accuracy ?? null, // Accuracy of the shot "classification": req?.body?.shot ?? null, // Type of shot "end_frame_idx": req?.body?.endFrame ?? null, // End frame index for the shot "hand": req?.body?.hand ?? null, // Hand used for the shot "shot_consistency": req?.body?.consistency ?? null, // Consistency of the shot "speed": req?.body?.speed ?? null, // Speed of the shot "spin_rate_left": req?.body?.spinRateLeft ?? null, // Spin rate for left spin "spin_rate_right": req?.body?.spinRateRight ?? null, // Spin rate for right spin "spin_type_left": req?.body?.spinRateLeftType ?? null, // Type of left spin "spin_type_right": req?.body?.spinRateRightType ?? null, // Type of right spin "start_frame_idx": req?.body?.frameIdx ?? null // Start frame index for the shot } ], "total_duration": req?.body?.duration // Total duration of the video } }; // Perform AI analysis on the manual data const aiAnalysisData = await getAIManualAnalysis(manualData); // If AI analysis data is returned, update the fields accordingly if (aiAnalysisData) { updatedFields.overall_metrics = aiAnalysisData.overall_metrics; // Update overall metrics updatedFields.total_duration = aiAnalysisData.total_duration; // Update total duration Object.assign(updatedFields, aiAnalysisData.shots); // Merge shot data into updated fields } // Check if overall metrics were not updated if (updatedFields?.overall_metrics === '') { const code = statusCodes.BAD_REQUEST; // Set response code to BAD_REQUEST const message = "Please upload the tennis specific video."; // Message for the user return makeResponse(res, code, false, message, {}); // Return error response } // Save the updated data to the database const data = await createUpload(updatedFields); // Prepare the updated data to return in the response const updatedData = { id: data?.id, // ID of the uploaded data user_id: data?.user_id, // User ID associated with the upload file_path: data?.file_path ? privateKey.NODE_SERVER_URL + data?.file_path // Construct full file path if available : '', total_duration: data?.total_duration, // Total duration of the upload upload_time: data?.upload_time, // Time of upload upload_day: data?.upload_day, // Day of upload overall_metrics: data?.overall_metrics ? JSON.parse(data?.overall_metrics) : {}, // Parse overall metrics if available }; // Return success response with updated data return makeResponse( res, statusCodes.SUCCESS, true, 'Manual form submitted successfully', // Success message updatedData // Include the updated data in the response ); } catch (error) { // Handle any errors that occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message // Return error response with appropriate status and message return makeResponse(res, code, false, message, {}); } }); export const getUserRecorderVideo = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT try { // Fetch current user details to check for changes const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Fetch the user's uploaded data const updatedData = await getUserUpload(userId); // Remove the password from the user data for security reasons delete updatedData.dataValues.password; // Construct the full URL for the profile picture if it exists updatedData.profile_pic = updatedData?.dataValues?.profile_pic ? privateKey.NODE_SERVER_URL + updatedData?.dataValues?.profile_pic : ""; // Filter and map the uploads to create simplified data updatedData.dataValues.simplifiedData = updatedData.uploads .filter((data) => { // Only keep records where overall_metrics exists and is valid try { const analysisData = JSON.parse(data.overall_metrics)?.analysis_json.shots; return analysisData && Array.isArray(analysisData) && analysisData.length > 0; } catch (e) { // In case of JSON parsing error, exclude this record return false; } }) .map((data) => { // Parse the analysis data to extract feedback const analysisData = JSON.parse(data.overall_metrics)?.analysis_json.shots; let feedBack = ""; if (analysisData) { // Accumulate feedback from each shot analysisData.forEach((shot) => { feedBack += shot.feedback + " "; // Concatenate feedback with a space }); } // Return a simplified object containing relevant data return { id: data.id, user_id: data.user_id, file_path: data.file_path ? privateKey.NODE_SERVER_URL + data.file_path : '', // Construct full file path total_duration: data.total_duration, upload_time: data.upload_time, feedBack: feedBack.trim() // Optionally trim to remove trailing space }; }); delete updatedData.dataValues.uploads; // Return the updated user data (excluding password) return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.USER_UPDATED, updatedData ); } catch (error) { const code = error?.code || statusCodes.SERVER_ERROR; const message = error?.message || responseMessages.SOMETHING_WRONG; return makeResponse(res, code, false, message, {}); } }); export const getAnalysisOfVideo = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT try { // Check if both query parameter 'value' and route parameter 'id' are provided if (!req.query.value && !req.params.id) { return makeResponse( res, statusCodes.INVALID_REQUEST, false, responseMessages.INVALID_PARAMETERS, {} ); } // Fetch current user details to verify their existence const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Fetch the uploaded video data by user ID and video ID const updatedData = await getUploadVideoById({ user_id: userId, id: req.params.id, }); // Check if the requested analysis data exists in the fetched video data if (updatedData.dataValues[req.query.value]) { // Parse the analysis data if it exists const result = JSON.parse(updatedData.dataValues[req.query.value]); return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.DATA_FETCHED, result // Return the fetched analysis data ); } else { // If the analysis data does not exist, return a success response with a failure message return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.SHOT_FAILURE, {} ); } } catch (error) { // Handle any errors that occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message return makeResponse(res, code, false, message, {}); // Return error response } }); // This will be the custom middleware to check the upload count export const checkUploadCount = async (req, res, next) => { try { const { userId } = req.user; // Get userId from decoded JWT // Count the number of uploads for this user const uploadCount = await countUserUploadsByUserId(userId); if (privateKey.NODE_ENV === 'development') { // Define the maximum number of uploads allowed (e.g., 3 uploads) const maxUploads = 15; if (uploadCount >= maxUploads) { return res.status(400).json({ message: `You have reached the maximum number of uploads (${maxUploads}).`, }); } } // If the upload count is within the limit, proceed to the next middleware next(); } catch (error) { console.error('Error checking upload count:', error); res .status(500) .json({ message: 'Server error while checking upload count.' }); } }; export const getUserLastRecorderVideo = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT try { // Fetch current user details to verify their existence const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Fetch the uploaded videos for the user const updatedData = await getUserUpload(userId); // Simplify the uploaded video data for easier access const simplifiedData = updatedData?.uploads ?.map((data) => ({ id: data?.id, user_id: data?.user_id, file_path: data?.file_path ? privateKey?.NODE_SERVER_URL + data?.file_path // Construct full file path : '', total_duration: data?.total_duration, upload_time: data?.upload_time, upload_day: data?.upload_day, // This should be like 'Monday', 'Tuesday', etc. overall_metrics: data?.overall_metrics ? JSON.parse(data?.overall_metrics) // Parse overall metrics if available : {}, })) .sort((a, b) => a.id - b.id); // Sort the simplified data by video ID // Get the last uploaded video details if available const lastUploadedVideo = simplifiedData.length > 0 ? { id: simplifiedData[simplifiedData.length - 1].id, user_id: simplifiedData[simplifiedData.length - 1].user_id, file_path: simplifiedData[simplifiedData.length - 1].file_path, total_duration: simplifiedData[simplifiedData.length - 1].total_duration, upload_time: simplifiedData[simplifiedData.length - 1].upload_time, upload_day: simplifiedData[simplifiedData.length - 1].upload_day, // This should be like 'Monday', 'Tuesday', etc. overall_metrics: simplifiedData[simplifiedData.length - 1]?.overall_metrics // Access overall metrics of the last video } : {}; // If no videos, return an empty object // Get the second last uploaded video details if available const lastSecondUploadedVideo = simplifiedData.length > 1 ? { id: simplifiedData[simplifiedData.length - 2]?.id, user_id: simplifiedData[simplifiedData?.length - 2]?.user_id, file_path: simplifiedData[simplifiedData?.length - 2]?.file_path, total_duration: simplifiedData[simplifiedData?.length - 2]?.total_duration, upload_time: simplifiedData[simplifiedData?.length - 2]?.upload_time, upload_day: simplifiedData[simplifiedData?.length - 2]?.upload_day, // This should be like 'Monday', 'Tuesday', etc. overall_metrics: simplifiedData[simplifiedData?.length - 2]?.overall_metrics // Access overall metrics of the second last video } : {}; // If no second last video, return an empty object // Extract the serve accuracy from both videos const lastServeAccuracy = lastUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "service")?.accuracy; const secondLastServeAccuracy = lastSecondUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "service")?.accuracy; // Calculate the percentage increase in serve accuracy const perIncServeAccuracy = ((lastServeAccuracy - secondLastServeAccuracy) / secondLastServeAccuracy) * 100; // Extract the backhand shot consistency from both videos const lastBackhandShotConsistency = lastUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "backhand")?.shot_consistency; const secondLastBackhandShotConsistency = lastSecondUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "backhand")?.shot_consistency; // Calculate the percentage increase in backhand shot consistency const perIncBackhandShotConsistency = ((lastBackhandShotConsistency - secondLastBackhandShotConsistency) / secondLastBackhandShotConsistency) * 100; // Extract the shot speed from both videos const lastShotSpeed = lastUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "smash")?.speed; const secondLastShotSpeed = lastSecondUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "smash")?.speed; // Calculate the percentage increase in shot speed const perIncShotSpeed = ((lastShotSpeed - secondLastShotSpeed) / secondLastShotSpeed) * 100; // Prepare a summary of the metrics const summary = { lastServeAccuracy: `${((lastServeAccuracy ?? 0.00) * 100).toFixed(2)}%`, // Format serve accuracy perIncServeAccuracy: `${(isNaN(perIncServeAccuracy) ? 0.00 : perIncServeAccuracy).toFixed(2)}%`, // Format percentage increase in serve accuracy lastBackhandShotConsistency: `${((lastBackhandShotConsistency ?? 0.00) * 100).toFixed(2)}%`, // Format backhand shot consistency perIncBackhandShotConsistency: `${(isNaN(perIncBackhandShotConsistency) ? 0.00 : perIncBackhandShotConsistency).toFixed(2)}%`, // Format percentage increase in backhand shot consistency lastShotSpeed: `${((lastShotSpeed ?? 0.00) * 100).toFixed(2)} km/h`, // Format shot speed perIncShotSpeed: `${(isNaN(perIncShotSpeed) ? 0.00 : perIncShotSpeed).toFixed(2)} km/h`, // Format percentage increase in shot speed } // Group records by upload_day (which is the day name like 'Monday', 'Tuesday', etc.) and count the occurrences const recordsByDay = simplifiedData.reduce((acc, data) => { const day = data.upload_day; // Group by upload_day (which is the day of the week name) if (!acc[day]) { acc[day] = 0; // Initialize counter if this day isn't in the accumulator } acc[day]++; // Increment the counter for that day return acc; // Return the updated accumulator }, {}); console.log(lastUploadedVideo) // Log the last uploaded video for debugging purposes // Calculate overall aggregate if uploads exist const overallAggregate = updatedData?.uploads?.length > 0 ? await calculateOverallAggregate( updatedData?.uploads ) : {}; // Return an empty object if no uploads // Return the response with the details of the last uploaded video and summary return makeResponse( res, statusCodes.SUCCESS, true, "Last User Video Details Fetched", // Success message { overallAggregatePercentage: overallAggregate, lastUploadedVideo, summary, recordsByDay, } ); } catch (error) { // Handle any errors that occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message return makeResponse(res, code, false, message, {}); // Return error response } }); export const getUserPerformanceStats = catchAsyncAction(async (req, res) => { const { userId } = req.user; // Get userId from decoded JWT try { // Fetch current user details to verify their existence const currentUser = await getUserById(userId); // If the user is not found, return a NOT_FOUND response if (!currentUser) { return makeResponse( res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {} ); } // Get the current date const currentDate = new Date(); // Calculate the start of the week (Monday) const startOfWeek = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay() + 1)); startOfWeek.setHours(0, 0, 0, 0); // Set to the start of the day // Calculate the end of the week (Sunday) const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(startOfWeek.getDate() + 6); endOfWeek.setHours(23, 59, 59, 999); // Set to the end of the day // Fetch the uploaded data for the current week const currentWeekData = await getUserUploadByDateFilter(userId, startOfWeek, endOfWeek); // Calculate the start of the last week (Monday of the previous week) const startOfLastWeek = new Date(startOfWeek); startOfLastWeek.setDate(startOfWeek.getDate() - 7); // Go back 7 days startOfLastWeek.setHours(0, 0, 0, 0); // Start of last week's Monday // Calculate the end of last week (Sunday of the previous week) const endOfLastWeek = new Date(startOfLastWeek); endOfLastWeek.setDate(startOfLastWeek.getDate() + 6); // Sunday of last week endOfLastWeek.setHours(23, 59, 59, 999); // End of last week's Sunday // Fetch the uploaded data for the last week const lastWeekData = await getUserUploadByDateFilter(userId, startOfLastWeek, endOfLastWeek); // Calculate overall aggregates for the current week if uploads exist const overallCurrentWeekAggregate = currentWeekData?.uploads?.length > 0 ? await calculateOverallAggregate( currentWeekData?.uploads ) : {}; // Calculate overall aggregates for the last week if uploads exist const overallLastWeekAggregate = lastWeekData?.uploads?.length > 0 ? await calculateOverallAggregate( lastWeekData?.uploads ) : {}; // Calculate percentage increases for various metrics const perIncAccuracyOverall = ((overallCurrentWeekAggregate?.avgAccuracy - overallLastWeekAggregate?.avgAccuracy) / overallLastWeekAggregate?.avgAccuracy); const perIncShotConsistencyOverall = ((overallCurrentWeekAggregate?.avgShotConsistency - overallLastWeekAggregate?.avgShotConsistency) / overallLastWeekAggregate?.avgShotConsistency); const perIncShotSpeedOverall = ((overallCurrentWeekAggregate?.avgSpeed - overallLastWeekAggregate?.avgSpeed) / overallLastWeekAggregate?.avgSpeed); const perIncSpinRateOverall = ((overallCurrentWeekAggregate?.avgSpinRateRight - overallLastWeekAggregate?.avgSpinRateRight) / overallLastWeekAggregate?.avgSpinRateRight); // Prepare a summary of the current week's performance metrics const overallCurrentWeekSummary = { perIncAccuracyOverall: `${(isNaN(perIncAccuracyOverall) ? 0.00 : perIncAccuracyOverall).toFixed(2)}%`, // Format accuracy increase perIncShotConsistencyOverall: `${(isNaN(perIncShotConsistencyOverall) ? 0.00 : perIncShotConsistencyOverall).toFixed(2)}%`, // Format shot consistency increase perIncSpinRateOverall: `${(isNaN(perIncSpinRateOverall) ? 0.00 : perIncSpinRateOverall).toFixed(2)}%`, // Format spin rate increase perIncShotSpeedOverall: `${(isNaN(perIncShotSpeedOverall) ? 0.00 : perIncShotSpeedOverall).toFixed(2)} km/h`, // Format shot speed increase } // Calculate overall improvement as an average percentage const overallImprovement = ( (perIncAccuracyOverall + perIncShotConsistencyOverall + perIncShotSpeedOverall + perIncSpinRateOverall) / 4 ).toFixed(2); // Round to 2 decimal places // Add overall improvement to the summary overallCurrentWeekSummary.overallImprovement = `${overallImprovement}%`; // Calculate metrics for the current and last week const currentWeekMetrics = currentWeekData?.uploads?.length > 0 ? await calculateShotMetrics( currentWeekData?.uploads ) : {}; const lastWeekMetrics = lastWeekData?.uploads?.length > 0 ? await calculateShotMetrics( lastWeekData?.uploads ) : {}; // Calculate percentage increases in specific shot metrics const perIncServeAccuracy = ((currentWeekMetrics?.averageServiceAccuracy - lastWeekMetrics?.averageServiceAccuracy) / lastWeekMetrics?.averageServiceAccuracy) * 100; const perIncBackhandConsistency = ((currentWeekMetrics?.averageBackhandConsistency - lastWeekMetrics?.averageBackhandConsistency) / lastWeekMetrics?.averageBackhandConsistency) * 100; const perIncShotSpeed = ((currentWeekMetrics?.averageShotSpeed - lastWeekMetrics?.averageShotSpeed) / lastWeekMetrics?.averageShotSpeed) * 100; // Prepare a summary of the current week's shot metrics const currentWeekSummary = { currentWeekServeAccuracy: `${((currentWeekMetrics?.totalServiceAccuracy ?? 0.00) * 100).toFixed(2)}%`, // Format current week serve accuracy perIncServeAccuracy: `${(isNaN(perIncServeAccuracy) ? 0.00 : perIncServeAccuracy).toFixed(2)}%`, // Format percentage increase in serve accuracy currentWeekBackhandConsistency: `${((currentWeekMetrics?.totalBackhandConsistency ?? 0.00) * 100).toFixed(2)}%`, // Format current week backhand consistency perIncBackhandConsistency: `${(isNaN(perIncBackhandConsistency) ? 0.00 : perIncBackhandConsistency).toFixed(2)}%`, // Format percentage increase in backhand consistency currentWeekShotSpeed: `${((currentWeekMetrics?.averageShotSpeed ?? 0.00)).toFixed(2)} km/h`, // Format current week shot speed perIncShotSpeed: `${(isNaN(perIncShotSpeed) ? 0.00 : perIncShotSpeed).toFixed(2)} km/h`, // Format percentage increase in shot speed } // Return the response with the user performance statistics return makeResponse( res, statusCodes.SUCCESS, true, "Details Fetched", // Success message { currentWeekSummary, overallCurrentWeekSummary, currentWeekServices: currentWeekMetrics?.services ?? [], // Include current week services lastWeekServices: lastWeekMetrics?.services ?? [], // Include last week services currentWeekSpins: currentWeekMetrics?.spinRightShots ?? [], // Include current week spin shots lastWeekSpins: lastWeekMetrics?.spinRightShots ?? [] // Include last week spin shots } ); } catch (error) { // Handle any errors that occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message return makeResponse(res, code, false, message, {}); // Return error response } }); async function calculateShotMetrics(uploads) { // Initialize totals and separate arrays let totalServiceAccuracy = 0; let totalBackhandConsistency = 0; let totalShotSpeed = 0; // Arrays to hold individual shot types let services = []; let spinRightShots = []; // Initialize counters for each shot type let serviceCount = 0; let backhandCount = 0; let shotCount = 0; // Loop through each upload and extract shot data from overall_metrics uploads?.forEach(upload => { upload.overall_metrics = JSON.parse(upload?.overall_metrics); const shots = upload?.overall_metrics?.analysis_json.shots; shots.forEach(shot => { // Check for "service" shot type if (shot?.classification === "service") { totalServiceAccuracy += shot?.accuracy; services.push(shot); // Add service shots to the 'services' array serviceCount++; // Increment service count } // Check for "backhand" shot type if (shot?.classification === "backhand") { totalBackhandConsistency += shot?.shot_consistency; backhandCount++; // Increment backhand count } // Check for "spin right" shot type if (shot?.classification === "spin right") { spinRightShots.push(shot); // Add spin right shots to the 'spinRightShots' array } // Add the speed of every shot to the total speed totalShotSpeed += shot?.speed; shotCount++; // Increment total shot count }); }); // Calculate averages const averageServiceAccuracy = serviceCount > 0 ? totalServiceAccuracy / serviceCount : 0; const averageBackhandConsistency = backhandCount > 0 ? totalBackhandConsistency / backhandCount : 0; const averageShotSpeed = shotCount > 0 ? totalShotSpeed / shotCount : 0; return { totalServiceAccuracy, totalBackhandConsistency, totalShotSpeed, averageServiceAccuracy, averageBackhandConsistency, averageShotSpeed, services, // Returning the array of service shots spinRightShots, // Returning the array of spin right shots uploads }; } // Function to calculate aggregate metrics for a single video async function calculateVideoMetrics(video) { const analysisData = video?.overall_metrics ? JSON.parse(video?.overall_metrics)?.analysis_json.shots : []; let totalAccuracy = 0; let totalShotConsistency = 0; let totalSpeed = 0; let totalSpinRateRight = 0; let feedBack = ""; analysisData.forEach((shot) => { totalAccuracy += shot.accuracy; totalShotConsistency += shot.shot_consistency; totalSpeed += shot.speed; totalSpinRateRight += shot.spin_rate_right; feedBack = feedBack + shot.feedback + " "; }); const numberOfShots = analysisData.length; // Calculate the average for each metric const avgAccuracy = totalAccuracy / numberOfShots; const avgShotConsistency = totalShotConsistency / numberOfShots; const avgSpeed = totalSpeed / numberOfShots; const avgSpinRateRight = totalSpinRateRight / numberOfShots; return { avgAccuracy, avgShotConsistency, avgSpeed, avgSpinRateRight, feedBack }; } // Function to calculate overall aggregate across all videos async function calculateOverallAggregate(videoData) { let totalAccuracy = 0; let totalShotConsistency = 0; let totalSpeed = 0; let totalSpinRateRight = 0; let totalShots = 0; let feedback = ""; // Use Promise.all to process all videos concurrently const metricsPromises = videoData?.map(async (video) => { const { avgAccuracy, avgShotConsistency, avgSpeed, avgSpinRateRight, feedBack } = await calculateVideoMetrics(video); totalAccuracy += avgAccuracy; totalShotConsistency += avgShotConsistency; totalSpeed += avgSpeed; totalSpinRateRight += avgSpinRateRight; feedback = feedBack totalShots++; }); // Wait for all promises to resolve await Promise.all(metricsPromises); // Calculate overall aggregate for all videos return { avgAccuracy: ((totalAccuracy / totalShots)).toFixed(2), avgShotConsistency: ((totalShotConsistency / totalShots)).toFixed(2), avgSpeed: ((totalSpeed / totalShots)).toFixed(2), avgSpinRateRight: ((totalSpinRateRight / totalShots)).toFixed(2), feedback }; } export const isQuizPlayed = catchAsyncAction(async (req, res) => { try { const { userId } = req.user; // Get userId from decoded JWT // Fetch the user's answers to determine if the quiz has been played const QuizPlayed = await getUserAnswersByUserId(userId); // Prepare response data indicating whether the quiz has been played const responseData = { isQuizPlayed: QuizPlayed.length > 0 ? true : false // Set to true if there are answers, otherwise false }; // Return the response with the quiz played status return makeResponse( res, statusCodes.SUCCESS, true, responseMessages.DATA_FETCHED, // Success message responseData // Include the response data ); } catch (error) { // Handle any errors that occur during the process const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message // Return error response with appropriate status and message return makeResponse(res, code, false, message, {}); } });