using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using RMuseum.Models.Auth.Memory; using RMuseum.Models.DiwanAudio; using RMuseum.Models.DiwanAudio.ViewModels; using RMuseum.Models.UploadSession; using RMuseum.Models.UploadSession.ViewModels; using RMuseum.Services; using RMuseum.Services.Implementation; using RSecurityBackend.Models.Generic; using RSecurityBackend.Services; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; namespace RMuseum.Controllers { [Produces("application/json")] [Route("api/audio")] public class RecitationController : Controller { /// /// returns paginated published recitations (if poetId or catId is non-zero its ordered by poemId ascending if not it is ordered by publish date descending) /// /// if PageSize is -1 or is more than 1000 it resets to 1000 /// empty: no search term, non-empty: searches within AudioArtist, AudioTitle, poem.FullTitle and poem.PlainText simultaneously /// /// /// [HttpGet("published")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPublished([FromQuery] PagingParameterModel paging, string searchTerm = "", int poetId = 0, int catId = 0) { if (paging.PageSize == -1 || paging.PageSize > 1000) paging.PageSize = 1000; var res = await _audioService.GetPublishedRecitations(paging, searchTerm, poetId, catId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); // Paging Header HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); return Ok(res.Result.Items); } /// /// get published recitation by id /// /// /// [HttpGet("published/{id}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(PublicRecitationViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task GetPublishedRecitationById(int id) { var res = await _audioService.GetPublishedRecitationById(id); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); if (res.Result == null) return NotFound(); return Ok(res.Result); } /// /// creates an RSS file from recent published recitations /// /// [HttpGet("published/rss")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetRssFeed() { int count = 200; var res = await _audioService.GetPublishedRecitations(new PagingParameterModel() { PageNumber = 1, PageSize = count}); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); string rss = RecitationsRssBuilder.Build(res.Result.Items); var stream = new MemoryStream(); var writer = new StreamWriter(stream); writer.Write(rss); writer.Flush(); stream.Position = 0; return new FileStreamResult(stream, "application/rss+xml"); } /// /// Gets audio narrations, user must have recitation::moderate permission to be able to see all users narrations /// /// /// default: false, user must have recitation::moderate permission to be able to see all users narrations /// default: -1, unfiltered /// /// /// /// additional headers: paging-headers, audio-upload-enabled /// [HttpGet] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task Get([FromQuery] PagingParameterModel paging, bool allUsers = false, AudioReviewStatus status = AudioReviewStatus.All, string searchTerm = "", bool mistakes = false, RecitationType recitationType = RecitationType.Unfiltered) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); if (allUsers) { RServiceResult canView = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(canView.ExceptionString)) return BadRequest(canView.ExceptionString); if (!canView.Result) return StatusCode((int)HttpStatusCode.Forbidden); } var res = await _audioService.SecureGetAll(paging, allUsers ? Guid.Empty : loggedOnUserId, status, searchTerm, mistakes, recitationType); if(!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); // Paging Header HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); //adding audio upload enabled header to reduce need for a separate query HttpContext.Response.Headers.Append("audio-upload-enabled", JsonConvert.SerializeObject(_audioService.UploadEnabled)); return Ok(res.Result.Items); } /// /// get the corresponding mp3 file for the narration /// /// /// it could be protected (Authorized), but I guess I would have problems with available client components support, /// so I preferred it to be anonymous, as it does not harm anybody I guess /// /// /// [HttpGet("file/{id}.mp3")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task GetMp3File(int id) { RServiceResult narration = await _audioService.Get(id); if (!string.IsNullOrEmpty(narration.ExceptionString)) return BadRequest(narration.ExceptionString); if (narration.Result == null) return NotFound(); DateTime lastModified = narration.Result.UploadDate;//TODO: Add a FileLastUpdated field to narrations to indicate the last time the mp3/xml files have been updated lastModified = new DateTime(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, lastModified.Kind); Response.GetTypedHeaders().LastModified = lastModified; Response.Headers.CacheControl = "public,max-age=86400"; var requestHeaders = Request.GetTypedHeaders(); if (requestHeaders.IfModifiedSince.HasValue && requestHeaders.IfModifiedSince.Value >= lastModified) { return StatusCode(StatusCodes.Status304NotModified); } HttpContext.Response.Headers.Append("Accept-Ranges", "bytes"); return new FileStreamResult(new FileStream(narration.Result.LocalMp3FilePath, FileMode.Open, FileAccess.Read), "audio/mpeg"); } /// /// get the corresponding xml file contents (xml) for the narration /// /// /// it could be protected (Authorized), but I guess I would have problems with available client components support, /// so I preferred it to be anonymous, as it does not harm anybody I guess /// /// /// [HttpGet("xml/{id}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task GetXMLContent(int id) { RServiceResult narration = await _audioService.Get(id); if (!string.IsNullOrEmpty(narration.ExceptionString)) return BadRequest(narration.ExceptionString); if (narration.Result == null) return NotFound(); DateTime lastModified = narration.Result.UploadDate;//TODO: Add a FileLastUpdated field to narrations to indicate the last time the mp3/xml files have been updated lastModified = new DateTime(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, lastModified.Kind); Response.GetTypedHeaders().LastModified = lastModified; Response.Headers.CacheControl = "public,max-age=86400"; var requestHeaders = Request.GetTypedHeaders(); if (requestHeaders.IfModifiedSince.HasValue && requestHeaders.IfModifiedSince.Value >= lastModified) { return StatusCode(StatusCodes.Status304NotModified); } return Ok(System.IO.File.ReadAllText(narration.Result.LocalXmlFilePath)); } /// /// get the corresponding xml file for the recitation /// /// /// it could be protected (Authorized), but I guess I would have problems with available client components support, /// so I preferred it to be anonymous, as it does not harm anybody I guess /// /// /// [HttpGet("file/{id}.xml")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetXMLFile(int id) { RServiceResult narration = await _audioService.Get(id); if (!string.IsNullOrEmpty(narration.ExceptionString)) return BadRequest(narration.ExceptionString); if (narration.Result == null) return NotFound(); DateTime lastModified = narration.Result.UploadDate;//TODO: Add a FileLastUpdated field to narrations to indicate the last time the mp3/xml files have been updated lastModified = new DateTime(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, lastModified.Kind); Response.GetTypedHeaders().LastModified = lastModified; Response.Headers.CacheControl = "public,max-age=86400"; var requestHeaders = Request.GetTypedHeaders(); if (requestHeaders.IfModifiedSince.HasValue && requestHeaders.IfModifiedSince.Value >= lastModified) { return StatusCode(StatusCodes.Status304NotModified); } return new FileStreamResult(new FileStream(narration.Result.LocalXmlFilePath, FileMode.Open, FileAccess.Read), "text/xml"); } /// /// Gets Verse Sync Information /// /// narration id /// [HttpGet("verses/{id}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RecitationVerseSync[]))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPoemNarrationVerseSyncArray(int id) { RServiceResult verses = await _audioService.GetPoemNarrationVerseSyncArray(id); if (!string.IsNullOrEmpty(verses.ExceptionString)) return BadRequest(verses.ExceptionString); return Ok(verses.Result); } /// /// updates narration metadata /// /// /// /// /// reviewstatus cannot be set to Approved or Rejected using this method, use moderate method instead /// only these set of fields are updatable: AudioTitle, AudioArtist, AudioArtistUrl, AudioSrc, AudioSrcUrl, ReviewStatus (Draft to Pending and vice versa and Approved/Rejected to Pending) /// only narrator or a moderator can update the narration /// /// [HttpPut("{id}")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RecitationViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task UpdatePoemNarration(int id, [FromBody] RecitationViewModel metadata) { if (!_audioService.UploadEnabled) return BadRequest("این قابلیت به دلیل تغییرات فنی سایت موقتاً غیرفعال است."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var narration = await _audioService.Get(id); if (!string.IsNullOrEmpty(narration.ExceptionString)) { return BadRequest(narration.ExceptionString); } if (narration.Result == null) return NotFound(); if(narration.Result.Owner.Id != loggedOnUserId) { Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); RServiceResult serviceResult = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(serviceResult.ExceptionString)) return BadRequest(serviceResult.ExceptionString); if (!serviceResult.Result) return StatusCode((int)HttpStatusCode.Forbidden); } if(narration.Result.ReviewStatus != metadata.ReviewStatus) { if (metadata.ReviewStatus == AudioReviewStatus.Approved || metadata.ReviewStatus == AudioReviewStatus.Rejected || metadata.ReviewStatus == AudioReviewStatus.RejectedDueToReportedErrors) { return StatusCode((int)HttpStatusCode.Forbidden); } } var res = await _audioService.UpdatePoemNarration(id, metadata); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// Moderate pending narration (for moderating other users' recitations you also need recitation:moderate permission) /// /// /// /// [HttpPut("moderate/{id}")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.PublishOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RecitationViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task ModeratePoemNarration(int id, [FromBody] RecitationModerateViewModel model) { if (!_audioService.UploadEnabled) return BadRequest("این قابلیت به دلیل تغییرات فنی سایت موقتاً غیرفعال است."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var narration = await _audioService.Get(id); if (!string.IsNullOrEmpty(narration.ExceptionString)) { return BadRequest(narration.ExceptionString); } if (narration.Result == null) return NotFound(); if(narration.Result.ReviewStatus == AudioReviewStatus.RejectedDueToReportedErrors) return StatusCode((int)HttpStatusCode.Forbidden); if (narration.Result.Owner.Id != loggedOnUserId) { Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); RServiceResult serviceResult = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(serviceResult.ExceptionString)) return BadRequest(serviceResult.ExceptionString); if (!serviceResult.Result) return StatusCode((int)HttpStatusCode.Forbidden); } if(narration.Result.RecitationType == RecitationType.Commentary) { if (narration.Result.Owner.Id == loggedOnUserId) { Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); RServiceResult serviceResult = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(serviceResult.ExceptionString)) return BadRequest(serviceResult.ExceptionString); if (!serviceResult.Result) { var recitation = (await _audioService.Get(narration.Result.Id)).Result; recitation.ReviewStatus = AudioReviewStatus.Pending; var resPendingModeration = await _audioService.UpdatePoemNarration(recitation.Id, recitation); if (!string.IsNullOrEmpty(resPendingModeration.ExceptionString)) { return BadRequest(resPendingModeration.ExceptionString); } return Ok(resPendingModeration.ExceptionString); } } } var res = await _audioService.ModeratePoemNarration(id, loggedOnUserId, model); if (!string.IsNullOrEmpty(res.ExceptionString)) { if(res.ExceptionString == "404") { return NotFound(); } return BadRequest(res.ExceptionString); } return Ok(res.Result); } /// /// Delete a recitation /// /// /// [HttpDelete("{id}")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task DeleteRecitation(int id) { if (!_audioService.UploadEnabled) return BadRequest("این قابلیت به دلیل تغییرات فنی سایت موقتاً غیرفعال است."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); RServiceResult res = await _audioService.Delete(id, loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } return Ok(res.Result); } /// /// Replace the synchronization (xml) file of an already approved recitation belonging to the /// logged on user, without touching its mp3 file. Send the new xml as the single file in the /// request's form-data. The uploaded xml is only accepted if its embedded PoemId and audio /// checksum match the target recitation exactly (i.e. it must be a resync of the very same mp3 /// already on record) - this endpoint never accepts a new mp3. On acceptance, the xml is queued /// for re-publishing to the external FTP server(s) in the background and you receive a /// notification with the final result (success or failure). /// /// recitation id /// [HttpPost("{id}/xml")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task ReplaceRecitationSyncXmlFile(int id) { try { if (!_audioService.UploadEnabled) return BadRequest("این قابلیت به دلیل تغییرات فنی سایت موقتاً غیرفعال است."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); IFormFile xmlFile = Request.Form.Files.FirstOrDefault(); RServiceResult resReplace = await _audioService.ReplaceRecitationSyncXmlFile(loggedOnUserId, id, xmlFile); if (!string.IsNullOrEmpty(resReplace.ExceptionString)) { if (resReplace.ExceptionString == "404") return NotFound(); return BadRequest(resReplace.ExceptionString); } return Ok(resReplace.Result); } catch (Exception exp) { return BadRequest(exp.ToString()); } } /// /// Get User Uploads /// /// /// default: false, user must have recitation::moderate permission to be able to see all users uploads /// /// additional headers: paging-headers, audio-upload-enabled [HttpGet("uploads")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task GetUploads([FromQuery] PagingParameterModel paging, bool allUsers = false) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); if (allUsers) { RServiceResult canView = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(canView.ExceptionString)) return BadRequest(canView.ExceptionString); if (!canView.Result) return StatusCode((int)HttpStatusCode.Forbidden); } var res = await _audioService.GetUploads(paging, allUsers ? Guid.Empty : loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); // Paging Header HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); //adding audio upload enabled header to reduce need for a separate query HttpContext.Response.Headers.Append("audio-upload-enabled", JsonConvert.SerializeObject(_audioService.UploadEnabled)); return Ok(res.Result.Items); } /// /// upload, update, moderate and delete operations on recitations might temporarily become disabled, /// this method gets the current status /// remarks: the value of this flag is provided as a custom header called audio-upload-enabled in some common GET methods /// in order to reduce the need for a separate query /// /// [HttpGet("uploadenabled")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] public IActionResult IsUploadEnabled() { return Ok(_audioService.UploadEnabled); } /// /// Reciation Upload /// /// /// if you send true to replace parameter, if there is an existing recitation for the poem from the user with the same Audio Artist name /// corresponding mp3+xml files are replaced an no other changes is applied (no new post, preserving recitation position) /// /// /// [HttpPost] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(UploadSession))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Unauthorized)] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task UploadReciation(bool replace = false, RecitationType recitationType = RecitationType.Normal) { try { if (!_audioService.UploadEnabled) return BadRequest("ارسال خوانش جدید به دلیل تغییرات فنی سایت موقتاً غیرفعال است."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); RServiceResult resSession = await _audioService.InitiateNewUploadSession(loggedOnUserId, replace); if (!string.IsNullOrEmpty(resSession.ExceptionString)) return BadRequest(resSession.ExceptionString); List files = new List(); foreach(IFormFile file in Request.Form.Files) { RServiceResult rsFileResult = await _audioService.SaveUploadedFile(file); if(!string.IsNullOrEmpty(rsFileResult.ExceptionString)) { return BadRequest(rsFileResult.ExceptionString); } files.Add(rsFileResult.Result); } resSession = await _audioService.FinalizeNewUploadSession(resSession.Result, recitationType, files.ToArray()); return Ok(resSession.Result); } catch (Exception exp) { return BadRequest(exp.ToString()); } } /// /// Get User Profiles /// /// /// [HttpGet("profile")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task GetUserNarrationProfiles(string artistName) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.GetUserNarrationProfiles(loggedOnUserId, artistName); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// Get User Default Profile /// /// [HttpGet("profile/def")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(UserRecitationProfileViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task GetUserDefProfile() { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.GetUserDefProfile(loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); if(res.Result == null) return BadRequest("نمایهٔ پیش‌فرض شما مشخص نیست. لطفاً پیش از ارسال خوانش از منو، قسمت «نمایه‌های من» نمایهٔ پیش‌فرض خود را تعریف کنید."); return Ok(res.Result); } /// /// Add a narration profile /// /// /// [HttpPost("profile")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(UserRecitationProfileViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task AddUserNarrationProfiles([FromBody]UserRecitationProfileViewModel profile) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); profile.UserId = loggedOnUserId; var res = await _audioService.AddUserNarrationProfiles(profile); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// Update a narration profile /// /// /// [HttpPut("profile")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(UserRecitationProfileViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task UpdateUserNarrationProfiles([FromBody] UserRecitationProfileViewModel profile) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); profile.UserId = loggedOnUserId; var res = await _audioService.UpdateUserNarrationProfiles(profile); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// Delete a narration profile /// /// /// [HttpDelete("profile/{id}")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task DeleteUserNarrationProfiles(Guid id) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); RServiceResult res = await _audioService.DeleteUserNarrationProfiles(id, loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } return Ok(res.Result); } /// /// publishing tracker data /// /// /// /// [HttpGet("publishqueue")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task GetPublishingQueueStatus([FromQuery] PagingParameterModel paging, bool unfinished) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); RServiceResult canViewAll = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(canViewAll.ExceptionString)) return BadRequest(canViewAll.ExceptionString); var res = await _audioService.GetPublishingQueueStatus(paging, unfinished, canViewAll.Result ? Guid.Empty : loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); // Paging Header HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); return Ok(res.Result.Items); } /// /// Transfer Recitations Ownership (for recitations owned by current user) /// /// /// /// number of transfered items [HttpPut("chown")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task TransferRecitationsOwnership(string targetEmailAddress, string artistName) { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var userSearchResult = await _appUserService.FindUserByEmail(targetEmailAddress); if (!string.IsNullOrEmpty(userSearchResult.ExceptionString)) return BadRequest(userSearchResult); if (userSearchResult.Result == null) return NotFound(); var resExec = await _audioService.TransferRecitationsOwnership(loggedOnUserId, (Guid)userSearchResult.Result.Id, artistName); if (!string.IsNullOrEmpty(resExec.ExceptionString)) return BadRequest(resExec.ExceptionString); return Ok(resExec.Result); } /// /// Synchronization Queue /// /// [HttpGet("syncqueue")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task GetSynchronizationQueue() { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); RServiceResult canViewAll = await _userPermissionChecker.Check ( loggedOnUserId, sessionId, User.Claims.Any(c => c.Type == "Language") ? User.Claims.First(c => c.Type == "Language").Value : "ur-PK", RMuseumSecurableItem.AudioRecitationEntityShortName, RMuseumSecurableItem.ModerateOperationShortName ); if (!string.IsNullOrEmpty(canViewAll.ExceptionString)) return BadRequest(canViewAll.ExceptionString); var res = await _audioService.GetSynchronizationQueue(canViewAll.Result ? Guid.Empty : loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// report an error in a recitation /// /// /// [HttpPost("errors/report")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RecitationErrorReportViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] public async Task ReportErrorAsync([FromBody] RecitationErrorReportViewModel report) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.ReportErrorAsync(loggedOnUserId, report); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get errors reported for recitations /// /// /// [HttpGet("errors/report")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetReportedErrorsAsync([FromQuery] PagingParameterModel paging) { var reports = await _audioService.GetReportedErrorsAsync(paging); if (!string.IsNullOrEmpty(reports.ExceptionString)) { return BadRequest(reports.ExceptionString); } // Paging Header HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(reports.Result.PagingMeta)); return Ok(reports.Result.Items); } /// /// reject a reported error for recitations and notify the reporter (and deletes the report) /// /// /// /// [HttpDelete] [Route("errors/report/{id}")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task RejectReportedErrorAsync(int id, string rejectionNote = "عدم تطابق با معیارهای حذف خوانش") { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); RServiceResult res = await _audioService.RejectReportedErrorAsync(id, rejectionNote); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } if (!res.Result) { return NotFound(); } return Ok(); } /// /// accepts a reported error for recitations, change status of the recitation to rejected and notify the reporter and recitation owner (and deletes the report) /// /// /// [HttpDelete] [Route("errors/report/accept/{id}")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task AcceptReportedErrorAsync(int id) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); RServiceResult res = await _audioService.AcceptReportedErrorAsync(id); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } if (!res.Result) { return NotFound(); } return Ok(); } /// /// accepts a reported error for recitations, add mistake to approve the mistake and notify the reporter and recitation owner (and deletes the report) /// /// /// [HttpPut] [Route("errors/report/save")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task AddReportToTheApprovedMistakesAsync([FromBody] RecitationErrorReportViewModel report) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); RServiceResult res = await _audioService.AddReportToTheApprovedMistakesAsync(report); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } if (!res.Result) { return NotFound(); } return Ok(); } /// /// edit approved mistake of a recitation text /// /// /// [HttpPut] [Route("errors/report/edit")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task EditApprovedMistakeAsync([FromBody] RecitationErrorReportViewModel report) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); RServiceResult res = await _audioService.EditApprovedMistakeAsync(report.Id, report.ReasonText); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } if (!res.Result) { return NotFound(); } return Ok(); } /// /// remove approved mistake /// /// /// [HttpDelete] [Route("errors/approved/{id}")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.Forbidden)] public async Task RemoveApprovedMistakeAsync(int id) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); RServiceResult res = await _audioService.RemoveApprovedMistakeAsync(id); if (!string.IsNullOrEmpty(res.ExceptionString)) { return BadRequest(res.ExceptionString); } return Ok(); } /// /// up vote a recitation /// /// /// [HttpPost("vote/{id}")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task UpVoteRecitationAsync(int id) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.UpVoteRecitationAsync(id, loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(); } /// /// revoke recitaion up vote /// /// /// [HttpDelete("vote/{id}")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task RevokeUpVoteFromRecitationAsync(int id) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.RevokeUpVoteFromRecitationAsync(id, loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(); } /// /// revoke recitaion up vote /// /// /// [HttpPut("vote/switch/{id}")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task SwitchRecitationUpVoteAsync(int id) { if (ReadOnlyMode) return BadRequest("سایت به دلایل فنی مثل انتقال سرور موقتاً در حالت فقط خواندنی قرار دارد. لطفاً ساعاتی دیگر مجدداً تلاش کنید."); Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.SwitchRecitationUpVoteAsync(id, loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get user upvoted recitations /// /// /// [HttpGet("votes")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetUserUpvotedRecitationsAsync([FromQuery] PagingParameterModel paging) { if (paging.PageSize == -1 || paging.PageSize > 1000) paging.PageSize = 1000; Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = await _audioService.GetUserUpvotedRecitationsAsync(paging, loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); // Paging Header HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); return Ok(res.Result.Items); } /// /// compute poem recitations order (no update) /// /// /// [HttpGet("votes/{poemId}/scores")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task ComputePoemRecitationsOrdersAsync(int poemId) { var res = await _audioService.ComputePoemRecitationsOrdersAsync(poemId, false); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// starts checking recitaions with missing files and add them to reported errors list job /// /// [HttpPost("healthcheck")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ModerateOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public IActionResult StartCheckingRecitationsHealthCheck() { Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); var res = _audioService.StartCheckingRecitationsHealthCheck(loggedOnUserId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(); } /// /// retry publish unpublished narrations /// /// [HttpPost] [Route("retrypublish")] [AllowAnonymous()] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task RetryPublish() { try { if (!_audioService.UploadEnabled) return BadRequest("این قابلیت به دلیل تغییرات فنی سایت موقتاً غیرفعال است."); await _audioService.RetryPublish(); return Ok(); } catch (Exception exp) { return BadRequest(exp.ToString()); } } /// /// Makes recitations of فریدون فرح‌اندوز first recitations /// /// [HttpPut("ff")] [Authorize(Policy = RMuseumSecurableItem.AudioRecitationEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.NotFound, Type = typeof(string))] public async Task MakeFFRecitationsFirst() { var resExec = await _audioService.MakeArtistRecitationsFirst("فریدون فرح‌اندوز"); if (!string.IsNullOrEmpty(resExec.ExceptionString)) return BadRequest(resExec.ExceptionString); return Ok(resExec.Result); } /// /// get category top one recitations /// /// /// /// [HttpGet("cattop1/{catId}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPoemCategoryTopRecitations(int catId, bool includePoemText) { var res = await _audioService.GetPoemCategoryTopRecitations(catId, includePoemText); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// check if a category has any recitations /// /// /// [HttpGet("catany/{catId}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPoemCategoryHasAnyRecitations(int catId) { var res = await _audioService.GetPoemCategoryHasAnyRecitations(catId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// rss for category top one recitations /// /// /// /// [HttpGet("cattop1/{catId}/rss")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPoemCategoryTopRecitationsRSS(int catId, bool includePoemText) { var res = await _audioService.GetPoemCategoryTopRecitations(catId, includePoemText); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); string rss = RecitationsRssBuilder.Build(res.Result); var stream = new MemoryStream(); var writer = new StreamWriter(stream); writer.Write(rss); writer.Flush(); stream.Position = 0; return new FileStreamResult(stream, "application/rss+xml"); } /// /// readonly mode /// public bool ReadOnlyMode { get { try { return bool.Parse(Configuration["ReadOnlyMode"]); } catch { return false; } } } /// /// constructor /// /// /// /// /// public RecitationController(IRecitationService audioService, IUserPermissionChecker userPermissionChecker, IAppUserService appUserService, IConfiguration configuration) { _audioService = audioService; _userPermissionChecker = userPermissionChecker; _appUserService = appUserService; } /// /// Artifact Service /// protected readonly IRecitationService _audioService; /// /// IUserPermissionChecker instance /// protected IUserPermissionChecker _userPermissionChecker; /// /// IAppUserService instance /// protected IAppUserService _appUserService; /// /// Configuration /// protected IConfiguration Configuration { get; } } }