AI-Tennis-Coach/server/controller/users/index.js
2025-02-11 11:23:59 +05:30

1390 lines
51 KiB
JavaScript

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, {});
}
});