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