1390 lines
51 KiB
JavaScript
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, {});
|
|
}
|
|
});
|