public async Task <ActionResult> Upload(HttpPostedFileBase mp3File, HttpPostedFileBase xmlFile, string userdir = "", string title = null, string learnerSkype = null) { int exerciseId = 0; int newUserId = 0; var userId = this.GetUserId(); // Save the recording if (mp3File != null) { ViewBag.mp3FileName = mp3File.FileName; // We can receive garbage from the wild web. try { using (var stream = mp3File.InputStream) { var duration = RecordingUtils.GetMp3Duration(stream); if (duration > 0) { var recordingTitle = !String.IsNullOrEmpty(title) ? title : Path.GetFileNameWithoutExtension(mp3File.FileName); exerciseId = await ExerciseUtils.SaveRecording( stream, userId, ArtifactType.Mp3, ServiceType.IeltsSpeaking, duration, recordingTitle ); } } } catch { } } // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided. if (exerciseId != 0 && this.GetUserIsTeacher()) { // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous. // Continue anyway with either old or new user. if (!String.IsNullOrEmpty(learnerSkype)) { newUserId = (await DapperHelper.QueryResilientlyAsync <int?>("dbo.exeTryChangeExerciseAuthor", new { ExerciseId = exerciseId, UserId = userId, SkypeName = learnerSkype, }, CommandType.StoredProcedure)) .SingleOrDefault() .GetValueOrDefault(); } IEnumerable <int> remarkSpots = new int[] { }; // If there is a remark spot collection XML file produced by the teacher coming with the recording. if (xmlFile != null) { // We can receive garbage from the wild web. try { XElement root; using (var stream = xmlFile.InputStream) { root = XElement.Load(stream); } var startedText = root.Attribute("time").Value; var started = DateTime.Parse(startedText, null, DateTimeStyles.RoundtripKind); remarkSpots = root .Elements("RemarkSpot") .Select(i => { var spot = DateTime.Parse(i.Attribute("time").Value, null, DateTimeStyles.RoundtripKind); return(Convert.ToInt32((spot - started).TotalMilliseconds)); }) .ToList(); } catch { } } // We have got remark spots. Create a review. if (remarkSpots.Any()) { var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview", new { ExerciseId = exerciseId, UserId = userId, }, CommandType.StoredProcedure )) .SingleOrDefault(); // Save remarks. if (reviewId != 0) { var pieces = remarkSpots .OrderBy(i => i) .Distinct() .Select((i, index) => { var finish = Math.Max(0, i - 1000); // We allow for a 1 second delay between the actual learner's mistake and the teacher's action. var start = Math.Max(0, finish - 2000); // Assume the spot is 2 second long. var remark = new RemarkSpot { ReviewId = reviewId, Type = ReviewPiece.PieceTypes.Remark, Id = index, Start = start, Finish = finish, }; return(new ReviewPiece { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index), Json = JsonUtils.SerializeAsJson(remark), }); }); var batchOperation = new TableBatchOperation(); foreach (var piece in pieces) { batchOperation.InsertOrReplace(piece); } // Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities. // The write entity will be deleted on review finish. var viewerEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId), }; batchOperation.InsertOrReplace(viewerEntity); var editorEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId), }; batchOperation.InsertOrReplace(editorEntity); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); await table.ExecuteBatchAsync(batchOperation); // Redirect to the reviews page. return(RedirectToAction("Edit", "Reviews", new { id = reviewId.ToString() })); } } // end of if (remarkSpots.Any()) } // end of if (exerciseId && this.GetIsTeacher()) // if (exerciseId != 0) { return(newUserId == 0 ? RedirectToAction("Index") : RedirectToAction("Index", "Reviews")); } ViewBag.Success = exerciseId != 0; ViewBag.UserDir = userdir; return(View()); } // end of Upload()
public async Task <IHttpActionResult> PutSave([FromUri] string uuid, [FromBody] JObject values) { string redirectToUrl = null; var learnerSkype = (string)values["learnerSkype"]; var titleValue = (string)values["title"]; var comment = (string)values["comment"]; var remarkSpots = ((JArray)values["remarkSpots"]).Select(i => (int)i); // milliseconds var userId = this.GetUserId(); //-- Call the remote transcoding service. var host = ConfigurationManager.AppSettings["RecorderHost"]; var urlFormat = "http://{0}/api/recordings/from_freeswitch/?uuid={1}&userId={2}"; var url = String.Format(urlFormat, host, uuid, userId); HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) { throw new HttpException("from_freeswitch"); } // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'RecordingInfo' from content with media type 'text/html'. var recordingDetails = await response.Content.ReadAsAsync <RecordingDetails>(); // Make sure the duration is known. If the transcoder has failed to parse the ffmpeg logs, it returns DurationMsec = 0. if (recordingDetails.TotalDuration == 0) { // Read the blob and try to determine the duration directly. recordingDetails.TotalDuration = await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Artifacts, recordingDetails.BlobName); } var title = !String.IsNullOrEmpty(titleValue) ? titleValue : Path.GetFileNameWithoutExtension(recordingDetails.BlobName); //-- Create a database record for the exercise. var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId, ServiceType.IeltsSpeaking, ArtifactType.Mp3, recordingDetails.TotalDuration, title, null, comment); // If the recording is being uploaded by a teacher, make it owned by the learner, create a review, and save remark spots if an accompanying XML file is provided. if (exerciseId != 0 && this.GetUserIsTeacher()) { //-- Change the exercise author. // We may succeed or fail with updating the user of the exercise depending on whether the provided Skype name is found and is unambiguous. // Continue anyway with either old or new user. var newUserId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeTryChangeExerciseAuthor", new { ExerciseId = exerciseId, UserId = userId, SkypeName = learnerSkype, }, CommandType.StoredProcedure)) .SingleOrDefault(); //-- Create a review. We do not charge the learner for a review initiated by the teacher. var reviewId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.exeCreateUploadedReview", new { ExerciseId = exerciseId, UserId = userId, }, CommandType.StoredProcedure)) .SingleOrDefault(); //-- We have got remark spots. Save remarks. if (reviewId != 0 && remarkSpots.Any()) { var pieces = remarkSpots .OrderBy(i => i) .Distinct() .Select((i, index) => { var finish = Math.Max(0, i - AssumedTeacherReactionDelayMSec); var start = Math.Max(0, finish - DefaultSpotLengthMSec); var remark = new RemarkSpot { ReviewId = reviewId, Type = ReviewPiece.PieceTypes.Remark, Id = index, Start = start, Finish = finish, }; return(new ReviewPiece { // This PartitionKey calculation is redundunt, but the overhead is neglectable. PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Remark, index), Json = JsonUtils.SerializeAsJson(remark), }); }); var batchOperation = new TableBatchOperation(); foreach (var piece in pieces) { batchOperation.InsertOrReplace(piece); } //-- Write entities which will allow the reviewer to access remarks for reading and writing. We will simply check the presence of one of these records as we read or write the entities. // The write entity will be deleted on review finish. var viewerEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Viewer, userId), }; batchOperation.InsertOrReplace(viewerEntity); var editorEntity = new ReviewPiece() { PartitionKey = ReviewPiece.GetPartitionKey(exerciseId), RowKey = ReviewPiece.GetRowKey(reviewId, ReviewPiece.PieceTypes.Editor, userId), }; batchOperation.InsertOrReplace(editorEntity); var table = AzureStorageUtils.GetCloudTable(AzureStorageUtils.TableNames.ReviewPieces); await table.ExecuteBatchAsync(batchOperation); } // end of if (reviewId != 0 && remarkSpots.Any()) //-- Return the link to the review page. The client will redirect. redirectToUrl = Url.Link("Default", new { Controller = "Reviews", Action = reviewId != 0 ? "Edit" : "Index", id = reviewId.ToString() }); } // end of if (exerciseId && this.GetIsTeacher()) return(Created(redirectToUrl, redirectToUrl)); }