Пример #1
0
        public async Task <IHttpActionResult> PutRecordingDetails([FromBody] JObject value)
        {
            // Execute the query even if the title is null, we need to send the ExerciseId back.
            var userId     = this.GetUserId();
            var recorderId = (string)value["recorderId"];
            var recordName = (string)value["recordName"];
            var blobName   = UploadUtils.ConstractArtifactBlobName(userId, recorderId, recordName);

            var title  = ExerciseUtils.NormalizeExerciseTitle((string)value["title"]);
            var cardId = (Guid?)value["cardId"];
            var length = UploadUtils.DurationToLength((double)value["duration"]); // We use duration because there is a chance that the same instance of a recorder may be reused.

            var sql        = @"
update dbo.exeExercises
set Title = @Title, CardId = @CardId
output deleted.Id
where UserId = @UserId 
    and [Length] = @Length 
    and Artifact = @Artifact;
";
            var exerciseId = (await DapperHelper.QueryResilientlyAsync <int>(sql,
                                                                             new
            {
                Title = title,
                CardId = cardId,
                UserId = userId,
                Length = length,
                Artifact = blobName,
            }))
                             .SingleOrDefault();

            return(Ok(exerciseId));
        }
Пример #2
0
        [ActionName("View")] // Be carefull, the term "View" has a special meaning in ASP.NET MVC. There is the method View() in the base Controller class.
        public async Task <ActionResult> ViewExercise(int id)
        {
            var exercises = await ExerciseUtils.GetExerciseWithReviews("dbo.exeGetExerciseWithReviews", new { UserId = this.GetUserId(), Id = id, });

            // We got a few rows of the same Exercise, joined with different Reviews. Group them into a single Exercise.
            var reviews = exercises
                          .SelectMany(i => i.Reviews)
                          .Where(i => i is ReviewDto) // Otherwise an absent review is deserialized by Dapper as null. i.e [null].
                          .OrderBy(i => i.RequestTime)
                          .ToList();                  // ToList() is needed, otherwise a mistical StackOverflow occurs within IIS.
            // Use the first Exercise as a single instance. Make it the common parent of all Reviews.
            var exercise = exercises.First();

            exercise.Reviews = reviews;

            ViewBag.ExerciseParam = exercise;
            ViewBag.cardIdParam   = exercise.CardId.GetValueOrDefault();
            if (exercise.CardId.HasValue)
            {
                ViewBag.cardParam = await ExerciseUtils.GetCardWithItems(exercise.CardId.Value);
            }

            switch (exercise.ArtifactType)
            {
            case ArtifactType.Jpeg:
                return(View("ViewPhoto"));

            case ArtifactType.Mp3:
                return(View("ViewRecording"));

            default:
                return(HttpNotFound(exercise.ArtifactType));
            }
        }
Пример #3
0
        public async Task <IHttpActionResult> GetUserCard(string serviceType)
        {
            var sql1   = @"
select CI.Id, CI.[Type], CI.Title, CI.CardId, CI.Position, CI.Content
from dbo.exeUserCards UC
	inner join dbo.exeCardsWithItems CI on UC.CardId = CI.Id
where UC.UserId = @UserId
	and UC.ServiceType = @ServiceType;
";
            var sql2   = @"
select Id, [Type], Title, CardId, Position, Content
from dbo.exeCardsWithItems
where Id = '4266FD13-32C6-412A-96EA-0B623FA82396';
";
            var userId = this.GetUserId();
            var card   = (await ExerciseUtils.GetCardsWithItems(
                              userId != 0 ? sql1 : sql2,
                              new
            {
                UserId = userId,
                ServiceType = serviceType
            }))
                         .FirstOrDefault();

            return(Ok(card));
        }
Пример #4
0
        public async Task <string> SaveAudiorRecording(string recorderId, string recordName, double duration)
        {
            bool success = false;

            try
            {
                var userId = this.GetUserId();
                // recorderId comes from Audior, 13 digits, milliseconds, current Date in the browser. It is part of Artifact and will be used to update Title with the real title after upload is done.
                var blobName = UploadUtils.ConstractArtifactBlobName(userId, recorderId, recordName);

                using (var stream = Request.InputStream)
                {
                    await AzureStorageUtils.UploadBlobAsync(stream, AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Mp3);
                }

                var length = UploadUtils.DurationToLength(duration);
                await ExerciseUtils.CreateExercise(blobName, userId, ServiceType.IeltsSpeaking, ArtifactType.Mp3, length, null);

                success = true;
            }
            catch (Exception ex)
            {
                return("save=failed\n" + ex.Message);
            }
            // Audior expects only "save=..." in response.
            return("save=" + (success ? "ok" : "failed"));
        }
Пример #5
0
        public async Task <IHttpActionResult> GetCards(string serviceType, string cardType)
        {
            var sql   = @"
select Id, [Type], Title, CardId, Position, Content
from dbo.exeCardsWithItems
where [Type] = @CardType;
";
            var cards = await ExerciseUtils.GetCardsWithItems(sql, new
            {
                CardType = CardType.GetCardType(serviceType, cardType)
            });

            return(Ok(cards));
        }
Пример #6
0
        public async Task <ActionResult> Claim(string id)
        {
            var userIdKey    = KeyUtils.IntToKey(0);
            var longTimeKey  = id + this.GetExtId();
            var blobName     = ExerciseUtils.FormatBlobName(userIdKey, longTimeKey, "metadata", "json");
            var blob         = AzureStorageUtils.GetBlob(AzureStorageUtils.ContainerNames.Artifacts, blobName);
            var metadataJson = await blob.DownloadTextAsync();

            var metadata         = JObject.Parse(metadataJson);
            var serviceType      = (string)metadata["serviceType"];
            var cardId           = (Guid?)metadata["cardId"];
            var title            = (string)metadata["title"];
            var comment          = (string)metadata["comment"];
            var details          = metadata["recordingDetails"];
            var recordingDetails = details.ToObject <RecordingDetails>();

            var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, this.GetUserId(),
                                                                serviceType, ArtifactType.Mp3, recordingDetails.TotalDuration, title, cardId, comment, details.ToString(Formatting.None));

            //~~ Redirect to the View exercise page.
            return(RedirectToAction("View", new { Id = exerciseId }));
        }
Пример #7
0
        public async Task <ActionResult> Edit(int id)
        {
            var exercise = (await ExerciseUtils.GetExerciseWithReviews("exeGetReview", new { UserId = this.GetUserId(), Id = id, })).Single();

            ViewBag.ExerciseParam = exercise;
            ViewBag.cardIdParam   = exercise.CardId.GetValueOrDefault();
            if (exercise.CardId.HasValue)
            {
                ViewBag.cardParam = await ExerciseUtils.GetCardWithItems(exercise.CardId.Value);
            }

            switch (exercise.ArtifactType)
            {
            case ArtifactType.Jpeg:
                return(View("EditWriting"));

            case ArtifactType.Mp3:
                return(View("EditRecording"));

            default:
                return(HttpNotFound(exercise.ArtifactType));
            }
        }
Пример #8
0
        public async Task <IHttpActionResult> PostTracks(string metadataName)
        {
            //~~ Validate.
            if (!Request.Content.IsMimeMultipartContent())
            {
                return(BadRequest());
            }

            // Parse the request body parts.
            var streamProvider = await Request.Content.ReadAsMultipartAsync();

            // Check mediaType of the files
            var mediaTypes = streamProvider.Contents
                             .Select(i => i.Headers)
                             .Where(i => i.ContentDisposition.Name.Trim('"') != metadataName)
                             .Select(i => i.ContentType)
                             //.Where(i => i != null) // ContentType is null in the 'metadata' part.
                             .Select(i => i.MediaType)
                             .Distinct()
            ;
            var mediaType          = mediaTypes.FirstOrDefault();
            var acceptedMediaTypes = new[] { MediaType.Mpeg, MediaType.Mp3, MediaType.Amr, MediaType.Gpp, MediaType.QuickTime };

            if ((mediaTypes.Count() != 1) || !acceptedMediaTypes.Contains(mediaType))
            {
                return(BadRequest(String.Join(", ", mediaTypes)));
            }

            //~~ Save the original media files to blobs.

            var userId    = this.GetUserId(); // May be 0 if the user is unauthenticated
            var userIdKey = KeyUtils.IntToKey(userId);
            var timeKey   = KeyUtils.GetTimeAsBase32();
            // The length of extId is 12 chars.
            var extId = (userId != 0)
                ? null
                : this.GetExtId();
            var longTimeKey = timeKey + extId; // If an operand of string concatenation is null, an empty string is substituted.
            var extension   = MediaType.GetExtension(mediaType);

            var tracks    = new List <KeyValuePair <string, MemoryStream> >();
            var fileNames = new List <string>();

            foreach (var content in streamProvider.Contents)
            {
                var name        = content.Headers.ContentDisposition.Name.Trim('"');
                var contentType = content.Headers.ContentType;
                // We have checked above, if ContentType has a value, it has the right MediaType.
                if ((name != metadataName) && (contentType != null))
                {
                    fileNames.Add(name);
                    var stream = await content.ReadAsStreamAsync();

                    using (stream)
                    {
                        var memStream = new MemoryStream();
                        await stream.CopyToAsync(memStream);

                        var pair = new KeyValuePair <string, MemoryStream>(name, memStream);
                        tracks.Add(pair);
                    }
                }
            }

            // Upload all blobs in parallel.
            var tasks = tracks.Select(i =>
            {
                var blobName = ExerciseUtils.FormatBlobName(userIdKey, longTimeKey, i.Key, extension);
                return(AzureStorageUtils.UploadBlobAsync(i.Value, AzureStorageUtils.ContainerNames.Artifacts, blobName, mediaType));
            });
            await Task.WhenAll(tasks);

            //~~ Call the transcoding service.

            ///* We send the file names as a comma separated list. There is also a binding in the Web API like this:
            //public IHttpActionResult GetFoo([FromUri] int[] ids); Call: /Foo?ids=1&ids=2&ids=3 or /Foo?ids[0]=1&ids[1]=2&ids[2]=3
            //public IHttpActionResult GetFoo([FromUri] List<string> ids); Call: /Foo?ids[]="a"&ids[]="b"&ids[]="c"
            //*/
            //var host = ConfigurationManager.AppSettings["RecorderHost"];
            //var urlFormat = "http://{0}/api/recordings/transcoded/?userIdKey={1}&timeKey={2}&extension={3}&fileNames={4}";
            //var url = String.Format(urlFormat, host, userIdKey, longTimeKey, extension, String.Join(",", fileNames));
            //HttpClient client = new HttpClient();
            //HttpResponseMessage response = await client.GetAsync(url);
            //if (!response.IsSuccessStatusCode)
            //{
            //    // return RedirectToAction(referrerAction, new { error = "transcoding_error" });
            //    return InternalServerError(new Exception("Transcoding error. " + response.StatusCode.ToString()));
            //}
            //// Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'JObject' 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 transcoder       = new RecordingTranscoder();
            var recordingDetails = await transcoder.Transcode(tracks, userIdKey, longTimeKey, extension);

            // Release the memory streams.
            tracks.ForEach(i =>
            {
                i.Value.Dispose();
            });

            //~~ Read the metadata.
            // Chrome wraps the part name in double-quotes.
            var metadataContent = streamProvider.Contents
                                  .Single(i => i.Headers.ContentDisposition.Name.Trim('"') == metadataName)
            ;
            var metadataJson = await metadataContent.ReadAsStringAsync();

            var metadata    = JObject.Parse(metadataJson);
            var serviceType = (string)metadata["serviceType"];
            var cardId      = (Guid?)metadata["cardId"];
            var title       = (string)metadata["title"];
            var comment     = (string)metadata["comment"];

            var serializer = new JsonSerializer()
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            };
            var details = JObject.FromObject(recordingDetails, serializer);

            if (userId != 0)
            {
                //~~ Create a database record.
                var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId,
                                                                    serviceType, ArtifactType.Mp3, recordingDetails.TotalDuration, title, cardId, comment, details.ToString(Formatting.None));

                //~~ The client will redirect to the View exercise page.
                return(Ok(new { ExerciseId = exerciseId }));
            }
            else
            {
                //~~ Save the details for future use.
                metadata.Add(new JProperty("recordingDetails", details));
                var blobName = ExerciseUtils.FormatBlobName(userIdKey, longTimeKey, "metadata", "json");
                await AzureStorageUtils.UploadTextAsync(metadata.ToString(), AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Json);

                //~~ The client will redirect to the Signup page. longTimeKey will be built from timeKey on the Claim page.
                return(Ok(new { Key = timeKey }));
            }
        }
Пример #9
0
        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()
Пример #10
0
        public async Task <ActionResult> SaveRecordingMobile(HttpPostedFileBase fileInput, string recorderId, string recordingTitle)
        {
            /*
             * 1. Save the original media file to a blob
             * 2. Call the remote Transcoding service. Pass the blob name.
             * 3. The Transcoding service saves the MP3 file to a blob and returns its name and the recording's duration.
             * 4. Create a database record.
             */
            if (fileInput == null)
            {
                return(RedirectToAction("RecordSpeech", new { error = "no_file" }));
            }

            //  return RedirectToAction("RecordSpeech", new { error = "test01" });

            var contentType         = fileInput.ContentType;
            var acceptedContentType = (new[] { MediaType.Amr, MediaType.Gpp, MediaType.QuickTime }).Contains(contentType);

            if (!acceptedContentType)
            {
                return(RedirectToAction("RecordSpeech", new { error = contentType }));
            }

            var userId = this.GetUserId();

            // 1. Save the original file.
            // The directory structure in Blob Storage is userIdKey/timeKey/originalFileName.ext
            var timeKey  = KeyUtils.GetTimeAsBase32();
            var fileName = fileInput.FileName;

            // Sanitize the fileName. Reserved URL characters must be properly escaped.
            fileName = String.IsNullOrWhiteSpace(fileName)
                ? timeKey
                : Uri.EscapeUriString(fileName.Trim());
            var invalidChars = Path.GetInvalidFileNameChars();

            fileName = new String(fileName.Select(i => invalidChars.Contains(i) ? '_' : i).ToArray());
            if (!Path.HasExtension(fileName))
            {
                Path.ChangeExtension(fileName, MediaType.GetExtension(contentType));
            }

            var blobName = KeyUtils.IntToKey(userId) + AzureStorageUtils.DefaultDirectoryDelimiter + timeKey + AzureStorageUtils.DefaultDirectoryDelimiter + fileName;

            using (var stream = fileInput.InputStream)
            {
                await AzureStorageUtils.UploadBlobAsync(stream, AzureStorageUtils.ContainerNames.Recordings, blobName, contentType);
            }

            // 2. Call the transcoding service.
            var                 host     = ConfigurationManager.AppSettings["RecorderHost"];
            var                 url      = String.Format("http://{0}/api/recordings/transcoded/?inputBlobName={1}", host, blobName);
            HttpClient          client   = new HttpClient();
            HttpResponseMessage response = await client.GetAsync(url);

            if (!response.IsSuccessStatusCode)
            {
                return(RedirectToAction("RecordSpeech", new { error = "transcoding_error" }));
            }
            // 3. Get the results from the service.
            // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'JObject' from content with media type 'text/html'.
            var value = await response.Content.ReadAsAsync <JObject>();

            var outputBlobName = (string)value["outputBlobName"];
            var duration       = Convert.ToDecimal((int)value["durationMsec"] / 1000.0m);

            // The transcoder may return -1 if it has failed to parse the ffmpeg logs.
            if (duration < 0)
            {
                // Read the blob and try to determine the duration directly.
                duration = await RecordingUtils.GetMp3Duration(AzureStorageUtils.ContainerNames.Recordings, outputBlobName);
            }
            // 4. Create a database record.
            var exerciseId = await ExerciseUtils.CreateExercise(outputBlobName, userId, ServiceType.IeltsSpeaking, ArtifactType.Mp3,
                                                                duration, recordingTitle);

            // 5. Redirect to the exercise page.
            return(RedirectToAction("View", new { Id = exerciseId }));
        }
        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));
        }
Пример #12
0
        public async Task <ActionResult> SavePhotos(IEnumerable <HttpPostedFileBase> files, IEnumerable <int> rotations, string serviceType, Guid?cardId, string title, string comment)
        {
            // Find out the action to redirect to on error. We use the referrer string. RedirectToAction seems accept a case-insensitive parameter.
            var referrerAction = (Request.UrlReferrer.Segments.Skip(2).Take(1).SingleOrDefault() ?? "Index").Trim('/');

            //referrerAction = referrerAction.First().ToString().ToUpper() + referrerAction.Substring(1).ToLower();
            // var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));

            if (files == null || files.All(i => i == null))
            {
                return(RedirectToAction(referrerAction, new { error = "no_file" }));
            }
            if (files.Count() != rotations.Count())
            {
                return(RedirectToAction(referrerAction, new { error = "wrong_rotations" }));
            }

            var items = files
                        .Select((i, idx) => new { File = i, Rotation = rotations.ElementAt(idx) })
                        // There may be empty form parts from input elements with no file selected.
                        .Where(i => i.File != null);

            if (!items.All(i => i.File.ContentType == MediaType.Jpeg))
            {
                return(RedirectToAction(referrerAction, new { error = "wrong_file_format" }));
            }

            var userId    = this.GetUserId();
            var userKey   = KeyUtils.IntToKey(userId);
            var timeKey   = KeyUtils.GetTimeAsBase32();
            var page      = 1; // Pages start counting from 1.
            var blobNames = new List <string>();

            foreach (var item in items)
            {
                using (var inputStream = item.File.InputStream)
                {
                    var blobName = String.Format("{0}/{1}/{2}.original.jpg", userKey, timeKey, page);
                    await AzureStorageUtils.UploadBlobAsync(inputStream, AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Jpeg);

                    using (var memoryStream = new MemoryStream())
                    {
                        /* I am not sure about using JpegBitmapDecoder. Because of the native code dependencies, the PresentationCore and WindowsBase assemblies need to be distributed as x86 and x64, so AnyCPU may be not possible? */
                        inputStream.Seek(0, SeekOrigin.Begin);
                        using (var image = Image.FromStream(inputStream))
                        {
                            RotateFlipType rotateFlipType = RotateFlipType.RotateNoneFlipNone;
                            switch (item.Rotation)
                            {
                            case -1:
                                rotateFlipType = RotateFlipType.Rotate270FlipNone;
                                break;

                            case 1:
                                rotateFlipType = RotateFlipType.Rotate90FlipNone;
                                break;

                            default:
                                break;
                            }
                            ;
                            if (rotateFlipType != RotateFlipType.RotateNoneFlipNone)
                            {
                                image.RotateFlip(rotateFlipType);
                            }
                            // We re-encode the image to decrease the size.
                            var codec         = ImageCodecInfo.GetImageEncoders().FirstOrDefault(c => c.FormatID == ImageFormat.Jpeg.Guid);
                            var encoderParams = new EncoderParameters(1);
                            // Highest quality is 100. Quality affects the file size. Do not change it until you have exprimented.
                            int quality = 50; // Do not pass inline. This parameter is passed via pointer and it has to be strongly typed.
                            encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, quality);
                            image.Save(memoryStream, codec, encoderParams);
                            //image.Save(memoryStream, ImageFormat.Jpeg);
                        }

                        memoryStream.Seek(0, SeekOrigin.Begin);
                        blobName = String.Format("{0}/{1}/{2}.jpg", userKey, timeKey, page);
                        await AzureStorageUtils.UploadBlobAsync(memoryStream, AzureStorageUtils.ContainerNames.Artifacts, blobName, MediaType.Jpeg);

                        blobNames.Add(blobName);
                    }

                    page++;
                }
            }

            var artifact = String.Join(",", blobNames);

            var exerciseId = await ExerciseUtils.CreateExercise(artifact, userId, serviceType, ArtifactType.Jpeg, 0, title, cardId, comment);

            return(RedirectToAction("View", "Exercises", new { Id = exerciseId }));
        }
Пример #13
0
        // TODO OBSOLETE. Do not rename the files parameter. The form inputs are bounded by this name.
        public async Task <ActionResult> SaveRecordings(IEnumerable <HttpPostedFileBase> files, string serviceType, Guid?cardId, string title, string comment)
        {
            /*
             * 1. Validate input.
             * 2. Save the original media files to blobs.
             * 3. Call the remote transcoding service. The service converts files to MP3, merges them into a single file, saves it to a blob and returns its name and the recording's duration.
             * 4. Create a database record.
             * 5. Redirect to the exercise page.
             */

            // 1. Validate input.

            // Find out the action to redirect to on error. We use the referrer string. RedirectToAction seems accept a case-insensitive parameter.
            var referrerAction = (Request.UrlReferrer.Segments.Skip(2).Take(1).SingleOrDefault() ?? "Index").Trim('/');

            //referrerAction = referrerAction.First().ToString().ToUpper() + referrerAction.Substring(1).ToLower();
            // var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));

            if (files == null || files.All(i => i == null))
            {
                return(RedirectToAction(referrerAction, new { error = "no_file" }));
            }

            // There may be empty form parts from input elements with no file selected.
            var originalFiles = files.Where(i => i != null);

            var contentType = originalFiles
                              .Select(i => i.ContentType)
                              .Distinct()
                              .Single()
            ;

            var acceptedContentTypes = (new[] { MediaType.Mpeg, MediaType.Mp3, MediaType.Amr, MediaType.Gpp, MediaType.QuickTime });

            if (!acceptedContentTypes.Contains(contentType))
            {
                return(RedirectToAction(referrerAction, new { error = "wrong_file_format" }));
            }

            // 2. Save the original media files to blobs.

            var userId    = this.GetUserId();
            var userIdKey = KeyUtils.IntToKey(userId);
            var timeKey   = KeyUtils.GetTimeAsBase32();
            var index     = 0;
            var extension = MediaType.GetExtension(contentType);

            //var blobNames = new List<string>();

            foreach (var file in originalFiles)
            {
                using (var inputStream = file.InputStream)
                {
                    // The directory structure in the Blob Storage is userIdKey/timeKey/index.ext. Runnymede.Helper.Controllers.RecordingsController.Get() relies on this structure.
                    var blobName = String.Format("{0}/{1}/{2}.{3}", userIdKey, timeKey, index, extension);
                    await AzureStorageUtils.UploadBlobAsync(inputStream, AzureStorageUtils.ContainerNames.Artifacts, blobName, file.ContentType);

                    index++;
                }
            }

            // 3. Call the remote transcoding service.

            var                 host      = ConfigurationManager.AppSettings["RecorderHost"];
            var                 urlFormat = "http://{0}/api/recordings/transcoded/?userIdKey={1}&timeKey={2}&extension={3}&count={4}";
            var                 url       = String.Format(urlFormat, host, userIdKey, timeKey, extension, originalFiles.Count());
            HttpClient          client    = new HttpClient();
            HttpResponseMessage response  = await client.GetAsync(url);

            if (!response.IsSuccessStatusCode)
            {
                return(RedirectToAction(referrerAction, new { error = "transcoding_error" }));
            }
            // Error is returned as HTML. Then we get error here: No MediaTypeFormatter is available to read an object of type 'JObject' 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);
            }

            // 4. Create a database record.
            var exerciseId = await ExerciseUtils.CreateExercise(recordingDetails.BlobName, userId,
                                                                serviceType, ArtifactType.Mp3, recordingDetails.TotalDuration, title, cardId, comment);

            // 5. Redirect to the exercise page.
            return(RedirectToAction("View", "Exercises", new { Id = exerciseId }));
        }