const express = require('express');
const auth = require('../middleware/authenticator.js');
const verify = require("../middleware/verifier.js");
const mysql = require("../db/mysql.js");
const PerformanceAnalyser = require("../Parser/PerformanceAnalyser.js");
const query = require("../util/query.js");
const clean = require('../Parser/cleaner.js');
const router = express.Router();
/**
* Route for getting all performance information about the given member
* @name member/get/all
* @function
* @inner
* @param {*} req
* @param {string} req.body.sheetMusicId The sheet music ID to be accessed.
* @param {*} res
* @returns {string} An error message if an error occured
* @async
*/
router.get('/all', auth.token, async (req,res) => {
verify.memberOfSheetMusic(req, res).then((choirId) => {
if (choirId === null) {
res.status(403).send('Unauthorized');
} else {
let sheetMusicId = req.query.sheetMusicId;
const errorMessage = "Unable to get exercises";
if (sheetMusicId) {
mysql.getClient().then((client) => {
client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
let memberId = results[0]["hex(member_id)"];
client.query("SELECT performance_data FROM tma.performance WHERE member_id=? AND sheet_music_id=?", [memberId, sheetMusicId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
res.status(200).send();
}
});
}
});
});
} else {
res.status(400).send(errorMessage);
}
}
}).catch((error) => {
res.status(403).send('Unauthorized');
})
});
/**
* Route for updating performance information without analyzing the results
* @name member/put/no-analysis
* @function
* @inner
* @param {*} req
* @param {string} req.body.sheetMusicId The sheet music ID to be accessed.
* @param {string} req.body.performanceData The stringified performance data to be added
* @param {string} req.body.performanceId The ID of the performance to which to append the latest performance data
* @param {*} res
* @returns {string} An error message if an error occured
* @async
*/
router.put('/no-analysis', auth.token, async(req,res) => {
verify.memberOfSheetMusic(req, res).then((choirId) => {
if (choirId === null) {
res.status(403).send('Unauthorized');
} else {
let performanceData = req.body.performanceData;
if (performanceData && typeof(performanceData) === 'string') {
performanceData = JSON.parse(performanceData);
}
let performanceId = req.body.performanceId
const errorMessage = "Unable to update performance";
if (performanceData && performanceId && performanceData.pitches.length > 0) {
mysql.getClient().then((client) => {
client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
let memberId = results[0]["hex(member_id)"];
client.query("SELECT performance_data FROM tma.performance WHERE performance_id=unhex(?) AND member_id=unhex(?)", [performanceId, memberId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else if (results.length === 0) {
res.status(204).send(errorMessage);
} else {
let currentPerformance = JSON.parse(results[0]["performance_data"]);
currentPerformance["pitches"] = currentPerformance["pitches"].concat(performanceData["pitches"]);
client.query("UPDATE tma.performance SET performance_data=? WHERE performance_id=unhex(?) AND member_id=unhex(?)", [JSON.stringify(currentPerformance), performanceId, memberId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
res.status(200).send();
}
});
}
});
}
});
});
} else {
res.status(400).send(errorMessage);
}
}
}).catch((error) => {
res.status(403).send('Unauthorized');
});
});
/**
* Route for updating performance information and analyzing the results
* @name member/put/analysis
* @function
* @inner
* @param {*} req
* @param {string} req.body.sheetMusicId The sheet music ID to be accessed
* @param {string} req.body.performanceData The stringified performance data to be added
* @param {string} req.body.performanceId The ID of the performance to which to append the latest performance data
* @param {string} req.body.sheetMusicId The sheet music that the performance is attached to
* @param {string} req.body.exerciseId If not null, gives the ID of the exercise that this performance was based on. Otherwise null
* @param {boolean} req.body.isDurationExercise Whether the performance was for a duration exercise
* @param {number} req.body.measureStart The start measure number of the performance
* @param {number} req.body.measureEnd The end measure number of the performance
* @param {*} res
* @returns {string} An error message if an error occured
* @async
*/
router.put("/analysis", auth.token, async(req,res) => {
verify.memberOfSheetMusic(req, res).then((choirId) => {
if (choirId === null) {
res.status(403).send('Unauthorized');
} else {
let performanceData = req.body.performanceData;
if (performanceData && typeof(performanceData) === 'string') {
performanceData = JSON.parse(performanceData);
}
let performanceId = req.body.performanceId;
let sheetMusicId = req.body.sheetMusicId;
let exerciseId = req.body.exerciseId;
let isDurationExercise = req.body.isDurationExercise;
let measureStart = req.body.measureStart;
let measureEnd = req.body.measureEnd;
const errorMessage = "Unable to update performance";
if (performanceId && performanceData && sheetMusicId && (exerciseId !== null || exerciseId === null) && performanceData.pitches.length > 0 && measureStart && measureEnd && (isDurationExercise || !isDurationExercise)) {
mysql.getClient().then((client) => {
client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
let memberId = results[0]["hex(member_id)"];
client.query("SELECT performance_data FROM tma.performance WHERE performance_id=unhex(?) AND member_id=unhex(?)", [performanceId, memberId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else if (results.length === 0) {
res.status(204).send(errorMessage);
} else {
let currentPerformance = JSON.parse(results[0]["performance_data"]);
currentPerformance["pitches"] = currentPerformance["pitches"].concat(performanceData["pitches"]);
client.query("UPDATE tma.performance SET performance_data=? WHERE performance_id=unhex(?) AND member_id=unhex(?)", [JSON.stringify(currentPerformance), performanceId, memberId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
client.query("SELECT part FROM tma.member_sheet_music_part WHERE sheet_music_id = unhex(?) AND member_id=unhex(?)", [sheetMusicId, memberId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else if (results.length === 0) {
res.status(204).send(errorMessage);
} else {
let partName = results[0]["part"];
client.query("SELECT performance_data FROM tma.sheet_music WHERE sheet_music_id = unhex(?)", [sheetMusicId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else if (results.length === 0) {
res.status(204).send(errorMessage);
} else {
let pData = JSON.parse(results[0]['performance_data']);
let measureBounds = null;
if (measureStart && measureEnd && measureStart !== null && measureEnd !== null) {
measureBounds= [measureStart, measureEnd];
}
let measureErrorExercise = exerciseId !== null ? isDurationExercise : false;
let measureErrors = cleanAndAnalyzePerformance(performanceData, pData, partName, 0, measureBounds, measureErrorExercise);
saveMeasureErrors(measureErrors, memberId, sheetMusicId).then((didSave) => {
if (!didSave) {
res.status(503).send(errorMessage);
} else {
res.status(200).send();
}
}).catch((error) => {
client.rollback(function() {
res.status(503).send(errorMessage);
});
});
}
});
}
});
}
});
}
});
}
});
});
} else {
res.status(400).send(errorMessage);
}
}
}).catch((error) => {
res.status(403).send('Unauthorized');
});
});
/**
* @typedef {object} MemberPostNewNoAnalysisPackage
* @property {string} performance_id The ID of the newly generated performance data
*/
/**
* Route for creating a new record of performance information without analyzing the results
* @name member/post/new/no-analysis
* @function
* @inner
* @param {*} req
* @param {string} req.body.sheetMusicId The sheet music ID to be accessed
* @param {string} req.body.performanceData The stringified performance data to be added
* @param {string} req.body.sheetMusicId The sheet music that the performance is attached to
* @param {string} req.body.exerciseId If not null, gives the ID of the exercise that this performance was based on. Otherwise null
* @param {boolean} req.body.isDurationExercise Whether the performance was for a duration exercise
* @param {number} req.body.measureStart The start measure number of the performance
* @param {number} req.body.measureEnd The end measure number of the performance
* @param {*} res
* @returns {MemberPostNewNoAnalysisPackage|string} On success the ID of the newly generated performance generated. Otherwise, an error message.
* @async
*/
router.post('/new/no-analysis', auth.token, async(req,res) => {
verify.memberOfSheetMusic(req, res).then((choirId) => {
if (choirId === null) {
res.status(403).send('Unauthorized');
} else {
let performanceData = req.body.performanceData;
if (performanceData && typeof(performanceData) === 'string') {
performanceData = JSON.parse(performanceData);
}
let sheetMusicId = req.body.sheetMusicId;
let exerciseId = req.body.exerciseId;
let isDurationExercise = req.body.isDurationExercise;
let measureStart = req.body.measureStart;
let measureEnd = req.body.measureEnd;
const errorMessage = "Unable to add performance";
if (performanceData && sheetMusicId && (exerciseId !== null || exerciseId === null) && performanceData.pitches.length > 0 && measureStart && measureEnd && (isDurationExercise || !isDurationExercise)) {
mysql.getClient().then((client) => {
client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
let memberId = results[0]["hex(member_id)"];
let lastValue;
let args = [JSON.stringify(performanceData), memberId];
if (exerciseId === null) {
lastValue = "sheet_music_id";
args.push(sheetMusicId);
} else {
lastValue = "exercise_id";
args.push(exerciseId);
}
client.query("SELECT replace(uuid(),\"-\",\"\") AS uuid", function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
let generated_uuid = results[0]["uuid"];
args.unshift(generated_uuid);
let sql = "insert into tma.performance (performance_id, performance_data, member_id, " + lastValue + ") values(unhex(?), ?, unhex(?), unhex(?))";
client.query(sql, args, function(error, results, fields) {
if (error) {
res.status(503).send(errorMessage);
} else {
res.status(200).json({ performance_id: generated_uuid });
}
});
}
});
}
});
});
} else {
res.status(400).send(errorMessage);
}
}
}).catch((error) => {
res.status(403).send('Unauthorized');
});
});
/**
* Route for creating a new record of performance information and analyzing the results
* @name member/post/new/analysis
* @function
* @inner
* @param {*} req
* @param {string} req.body.sheetMusicId The sheet music ID to be accessed
* @param {string} req.body.performanceData The stringified performance data to be added
* @param {string} req.body.sheetMusicId The sheet music that the performance is attached to
* @param {string} req.body.exerciseId If not null, gives the ID of the exercise that this performance was based on. Otherwise null
* @param {boolean} req.body.isDurationExercise Whether the performance was for a duration exercise
* @param {number} req.body.measureStart The start measure number of the performance
* @param {number} req.body.measureEnd The end measure number of the performance
* @param {*} res
* @returns {string} An error message if an error occured
* @async
*/
router.post('/new/analysis', auth.token, async (req,res) => {
verify.memberOfSheetMusic(req, res).then((choirId) => {
if (choirId === null) {
res.status(403).send('Unauthorized');
} else {
let performanceData = req.body.performanceData;
if (performanceData && typeof(performanceData) === 'string') {
performanceData = JSON.parse(performanceData);
}
let sheetMusicId = req.body.sheetMusicId;
let exerciseId = req.body.exerciseId;
let isDurationExercise = req.body.isDurationExercise;
let measureStart = req.body.measureStart;
let measureEnd = req.body.measureEnd;
const errorMessage = "Unable to add performance";
if (performanceData && sheetMusicId && (exerciseId !== null || exerciseId === null) && performanceData.pitches.length > 0 && measureStart && measureEnd && (isDurationExercise || !isDurationExercise)) {
mysql.getClient().then((client) => {
client.beginTransaction(function(error) {
client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
if (error) {
client.rollback(function() {
res.status(503).send(errorMessage);
});
} else {
let memberId = results[0]["hex(member_id)"];
client.query("SELECT part FROM tma.member_sheet_music_part WHERE sheet_music_id = unhex(?) AND member_id=unhex(?)", [sheetMusicId, memberId], function(error, results, fields) {
if (error) {
client.rollback(function() {
res.status(503).send(errorMessage);
});
} else if (results.length === 0) {
client.rollback(function() {
res.status(204).send(errorMessage);
});
} else {
let partName = results[0]["part"];
client.query("SELECT performance_data FROM tma.sheet_music WHERE sheet_music_id = unhex(?)", [sheetMusicId], function(error, results, fields) {
if (error) {
client.rollback(function() {
res.status(503).send(errorMessage);
});
} else if (results.length === 0) {
client.rollback(function() {
res.status(204).send(errorMessage);
});
} else {
let pData = JSON.parse(results[0]['performance_data']);
let measureBounds = null;
if (measureStart && measureEnd && measureStart !== null && measureEnd !== null) {
measureBounds= [measureStart, measureEnd];
}
let measureErrorExercise = exerciseId !== null ? isDurationExercise : false;
let measureErrors = cleanAndAnalyzePerformance(performanceData, pData, partName, 0, measureBounds, measureErrorExercise);
saveMeasureErrors(measureErrors, memberId, sheetMusicId).then((didSave) => {
if (!didSave) {
client.rollback(function() {
res.status(503).send(errorMessage);
});
} else {
let lastValue;
let args = [JSON.stringify(performanceData), memberId];
if (exerciseId === null) {
lastValue = "sheet_music_id";
args.push(sheetMusicId);
} else {
lastValue = "exercise_id";
args.push(exerciseId);
}
let sql = "insert into tma.performance (performance_id, performance_data, member_id, " + lastValue + ") values(unhex(replace(uuid(),\"-\",\"\")), ?, unhex(?), unhex(?))";
client.query(sql, args, function(error, results, fields) {
if (error) {
client.rollback(function() {
res.status(503).send(errorMessage);
});
} else {
client.commit(function(err) {
if (err) {
client.rollback(function() {
res.status(503).send(errorMessage);
});
} else {
res.status(200).send();
}
});
}
});
}
}).catch((error) => {
client.rollback(function() {
res.status(503).send(errorMessage);
});
});
}
});
}
});
}
});
});
});
} else {
res.status(400).send(errorMessage);
}
}
}).catch((error) => {
res.status(403).send('Unauthorized');
});
});
/**
* Saves the new result of measure errors into the list for the given member of the given sheet music
* @param {AnalyzedPerformancePackage[]} measureErrors An array of analyzed performance objects with information about each measure analyzed
* @param {string} memberId The ID of the member to be updated
* @param {string} sheetMusicId The ID of the sheet music that the performance was for
* @returns {boolean} If the saving was successful
* @private
*/
const saveMeasureErrors = async(measureErrors, memberId, sheetMusicId) => {
try {
let obj_1 = await query("SELECT progress_data FROM tma.performance_progress WHERE member_id=unhex(?) AND sheet_music_id=unhex(?)", [memberId, sheetMusicId]);
if (obj_1.results.length === 0) {
return false;
} else {
let progress = JSON.parse(obj_1.results[0]["progress_data"]);
let currentIndex = progress.index;
let newIndex = (progress.index + 1) % 5;
if (progress.data.length === 5) {
for (let i = 0; i < measureErrors.length; i++) {
progress.data[newIndex][measureErrors[i]["measureNumber"] - 1] = measureErrors[i]["overallScore"];
}
} else {
let newProgressData = JSON.parse(JSON.stringify(progress.data[currentIndex]));
for (let i = 0; i < measureErrors.length; i++) {
newProgressData[measureErrors[i]["measureNumber"] - 1] = measureErrors[i]["overallScore"];
}
progress.data.push(newProgressData);
}
progress.index = newIndex;
await query("UPDATE tma.performance_progress SET progress_data=? WHERE member_id=unhex(?) AND sheet_music_id=unhex(?)", [JSON.stringify(progress), memberId, sheetMusicId]);
return true;
}
} catch(error) {
return false;
}
}
/**
* Cleans provided performance data and analyzes it comparing to the provided sheet music data of the specified track and staff number within the measure bounds provided
* @param {PerformancePackage} performanceData - The performance object derivied from user performance
* @param {ParsedOutputPackage} sheetMusicData - Internalized object based on alphaTex for sheet music
* @param {string} partName - The name of the part to be analyzed in the sheet music
* @param {number} staffIndex - The number of the staff to be analyzed
* @param {int[]} measureBounds - The bounds of the measures or null for all
* @param {Boolean} isDurationExercise - Whether this was a duration exercise performance
* @return {AnalyzedPerformancePackage[]} An array of analyzed performance objects with information about each measure analyzed
* TODO: Make overall score based on duration as well
*/
const cleanAndAnalyzePerformance = (performanceData, sheetMusicData, partName, staffIndex, measureBounds, isDurationExercise) => {
let pitches = clean(performanceData);
let results = PerformanceAnalyser.analyzePerformance({pitches}, sheetMusicData, partName, staffIndex, measureBounds, isDurationExercise);
return results;
}
module.exports = router;