From 47011429f5cc49e1013e346158d5a9d4efd07185 Mon Sep 17 00:00:00 2001 From: Hamid Reza Mohammadi Date: Thu, 17 Sep 2026 21:08:41 +0330 Subject: [PATCH] replace resitation --- RMuseum/Controllers/RecitationController.cs | 39 +++++ RMuseum/RMuseum.xml | 42 +++++ RMuseum/Services/IRecitationService.cs | 13 ++ .../Implementation/RecitationService.cs | 155 ++++++++++++++++++ 4 files changed, 249 insertions(+) diff --git a/RMuseum/Controllers/RecitationController.cs b/RMuseum/Controllers/RecitationController.cs index 074fc304..5587e7c3 100644 --- a/RMuseum/Controllers/RecitationController.cs +++ b/RMuseum/Controllers/RecitationController.cs @@ -488,6 +488,45 @@ namespace RMuseum.Controllers 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()); + } + } /// diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml index 54f1d807..9565bb4f 100644 --- a/RMuseum/RMuseum.xml +++ b/RMuseum/RMuseum.xml @@ -3139,6 +3139,19 @@ + + + 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 + + Get User Uploads @@ -23481,6 +23494,22 @@ + + + Replace only the synchronization (xml) file of an existing, approved recitation owned by the user. + The recitation's mp3 file is left untouched. The uploaded xml's embedded PoemId and audio file + checksum must match the target recitation's GanjoorPostId and Mp3FileCheckSum respectively (i.e. + it must be a resync of the very same, already uploaded, mp3 file) or the request is rejected - + this method never accepts a new mp3, only a corrected/retimed xml for the existing one. + On success the updated xml (and the unchanged mp3) are re-published to the external/backup FTP + servers in the background and the user is notified of the final result (success or failure), + reusing the exact same publishing/notification pipeline used for normal recitation uploads. + + must be the recitation owner + + + + Gets Verse Sync Range Information @@ -24064,6 +24093,19 @@ + + + Replace only the synchronization (xml) file of an existing, approved recitation owned by the + user. The recitation's mp3 file is left untouched; the uploaded xml's embedded PoemId and audio + checksum must match the target recitation or the request is rejected. On success, the updated + xml is re-published to the external/backup FTP servers in the background and the user is + notified of the final result (success or failure). + + must be the recitation owner + + + + Gets Verse Sync Range Information diff --git a/RMuseum/Services/IRecitationService.cs b/RMuseum/Services/IRecitationService.cs index 7aed8d17..573677f9 100644 --- a/RMuseum/Services/IRecitationService.cs +++ b/RMuseum/Services/IRecitationService.cs @@ -73,6 +73,19 @@ namespace RMuseum.Services /// Task> Delete(int id, Guid userId); + /// + /// Replace only the synchronization (xml) file of an existing, approved recitation owned by the + /// user. The recitation's mp3 file is left untouched; the uploaded xml's embedded PoemId and audio + /// checksum must match the target recitation or the request is rejected. On success, the updated + /// xml is re-published to the external/backup FTP servers in the background and the user is + /// notified of the final result (success or failure). + /// + /// must be the recitation owner + /// + /// + /// + Task> ReplaceRecitationSyncXmlFile(Guid userId, int recitationId, IFormFile xmlFile); + /// /// Gets Verse Sync Range Information /// diff --git a/RMuseum/Services/Implementation/RecitationService.cs b/RMuseum/Services/Implementation/RecitationService.cs index 73d2f8d9..b8852499 100644 --- a/RMuseum/Services/Implementation/RecitationService.cs +++ b/RMuseum/Services/Implementation/RecitationService.cs @@ -496,6 +496,161 @@ namespace RMuseum.Services.Implementationa return new RServiceResult(true); } + /// + /// Replace only the synchronization (xml) file of an existing, approved recitation owned by the user. + /// The recitation's mp3 file is left untouched. The uploaded xml's embedded PoemId and audio file + /// checksum must match the target recitation's GanjoorPostId and Mp3FileCheckSum respectively (i.e. + /// it must be a resync of the very same, already uploaded, mp3 file) or the request is rejected - + /// this method never accepts a new mp3, only a corrected/retimed xml for the existing one. + /// On success the updated xml (and the unchanged mp3) are re-published to the external/backup FTP + /// servers in the background and the user is notified of the final result (success or failure), + /// reusing the exact same publishing/notification pipeline used for normal recitation uploads. + /// + /// must be the recitation owner + /// + /// + /// + public async Task> ReplaceRecitationSyncXmlFile(Guid userId, int recitationId, IFormFile xmlFile) + { + if (xmlFile == null || xmlFile.Length == 0) + { + return new RServiceResult(false, "فایل xml ارسال نشده است."); + } + if (Path.GetExtension(xmlFile.FileName).ToLower() != ".xml") + { + return new RServiceResult(false, "تنها فایل با پسوند xml قابل پذیرش است."); + } + + Recitation recitation = await _context.Recitations.Where(r => r.Id == recitationId && r.OwnerId == userId).FirstOrDefaultAsync(); + if (recitation == null) + { + return new RServiceResult(false, "404"); + } + if (recitation.ReviewStatus != AudioReviewStatus.Approved) + { + await _notificationService.PushNotification + ( + userId, + "خطا در جایگزینی فایل xml", + $"جایگزینی فایل xml تنها برای خوانش‌های تأیید شده امکان‌پذیر است.{Environment.NewLine}" + + $"{recitation.AudioTitle}" + , NotificationType.Error + ); + return new RServiceResult(false, "تنها امکان جایگزینی فایل xml برای خوانش‌های تأیید شده وجود دارد."); + } + + RServiceResult savedFile = await SaveUploadedFile(xmlFile); + if (!string.IsNullOrEmpty(savedFile.ExceptionString)) + { + return new RServiceResult(false, savedFile.ExceptionString); + } + string tempXmlFilePath = savedFile.Result.FilePath; + + List parsedAudioList; + try + { + parsedAudioList = PoemAudioListProcessor.Load(tempXmlFilePath); + } + catch (Exception exp) + { + if (File.Exists(tempXmlFilePath)) + { + File.Delete(tempXmlFilePath); + } + await _notificationService.PushNotification + ( + userId, + "خطا در جایگزینی فایل xml", + $"خطا در خواندن فایل xml ارسالی برای {recitation.AudioTitle}: {exp.Message}" + , NotificationType.Error + ); + return new RServiceResult(false, $"خطا در خواندن فایل xml: {exp.Message}"); + } + + PoemAudio audio = parsedAudioList.FirstOrDefault(); + if (audio == null) + { + if (File.Exists(tempXmlFilePath)) + { + File.Delete(tempXmlFilePath); + } + await _notificationService.PushNotification + ( + userId, + "خطا در جایگزینی فایل xml", + $"فایل xml ارسالی برای {recitation.AudioTitle} اطلاعات معتبری ندارد." + , NotificationType.Error + ); + return new RServiceResult(false, "فایل xml اطلاعات معتبری ندارد."); + } + + if (audio.PoemId != recitation.GanjoorPostId) + { + if (File.Exists(tempXmlFilePath)) + { + File.Delete(tempXmlFilePath); + } + string msg = $"شناسهٔ شعر درج شده در فایل xml ({audio.PoemId}) با شناسهٔ شعر خوانش انتخابی ({recitation.GanjoorPostId} - {recitation.AudioTitle}) همخوانی ندارد."; + await _notificationService.PushNotification + ( + userId, + "خطا در جایگزینی فایل xml", + msg + , NotificationType.Error + ); + return new RServiceResult(false, msg); + } + + if (audio.FileCheckSum != recitation.Mp3FileCheckSum) + { + if (File.Exists(tempXmlFilePath)) + { + File.Delete(tempXmlFilePath); + } + string msg = "این فایل xml برای فایل mp3 دیگری همگام‌سازی شده و با فایل صوتی خوانش انتخابی مطابقت ندارد." + + $"{Environment.NewLine}این قابلیت تنها برای جایگزینی فایل xml مربوط به همان فایل mp3 موجود است؛ " + + "برای تغییر خود فایل mp3 از روش معمول ارسال خوانش (با گزینهٔ جایگزینی) استفاده کنید."; + await _notificationService.PushNotification + ( + userId, + "خطا در جایگزینی فایل xml", + $"{recitation.AudioTitle}{Environment.NewLine}{msg}" + , NotificationType.Error + ); + return new RServiceResult(false, msg); + } + + File.Move(tempXmlFilePath, recitation.LocalXmlFilePath, true); + + recitation.FileLastUpdated = DateTime.Now; + recitation.AudioSyncStatus = AudioSyncStatus.SoundOrXMLFilesChanged; + _context.Recitations.Update(recitation); + await _context.SaveChangesAsync(); + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) //this is long running job, so _context might be already been freed/collected by GC + { + RecitationPublishingTracker tracker = new RecitationPublishingTracker() + { + PoemNarrationId = recitation.Id, + StartDate = DateTime.Now, + XmlFileCopied = false, + Mp3FileCopied = false, + FirstDbUpdated = false, + SecondDbUpdated = false, + }; + context.RecitationPublishingTrackers.Add(tracker); + await context.SaveChangesAsync(); + + await _PublishNarration(recitation, tracker, context); + } + }); + + return new RServiceResult(true); + } /// /// Gets Verse Sync Range Information