replace resitation

This commit is contained in:
Hamid Reza Mohammadi 2026-09-17 21:08:41 +03:30
parent 5344ea0b82
commit 47011429f5
4 changed files with 249 additions and 0 deletions

View File

@ -488,6 +488,45 @@ namespace RMuseum.Controllers
return Ok(res.Result); return Ok(res.Result);
} }
/// <summary>
/// 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).
/// </summary>
/// <param name="id">recitation id</param>
/// <returns></returns>
[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<IActionResult> 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<bool> 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());
}
}
/// <summary> /// <summary>

View File

@ -3139,6 +3139,19 @@
<param name="id"></param> <param name="id"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.RecitationController.ReplaceRecitationSyncXmlFile(System.Int32)">
<summary>
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).
</summary>
<param name="id">recitation id</param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.RecitationController.GetUploads(RSecurityBackend.Models.Generic.PagingParameterModel,System.Boolean)"> <member name="M:RMuseum.Controllers.RecitationController.GetUploads(RSecurityBackend.Models.Generic.PagingParameterModel,System.Boolean)">
<summary> <summary>
Get User Uploads Get User Uploads
@ -23481,6 +23494,22 @@
<param name="userId"></param> <param name="userId"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.Implementationa.RecitationService.ReplaceRecitationSyncXmlFile(System.Guid,System.Int32,Microsoft.AspNetCore.Http.IFormFile)">
<summary>
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.
</summary>
<param name="userId">must be the recitation owner</param>
<param name="recitationId"></param>
<param name="xmlFile"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementationa.RecitationService.GetPoemNarrationVerseSyncArray(System.Int32)"> <member name="M:RMuseum.Services.Implementationa.RecitationService.GetPoemNarrationVerseSyncArray(System.Int32)">
<summary> <summary>
Gets Verse Sync Range Information Gets Verse Sync Range Information
@ -24064,6 +24093,19 @@
<param name="userId"></param> <param name="userId"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.IRecitationService.ReplaceRecitationSyncXmlFile(System.Guid,System.Int32,Microsoft.AspNetCore.Http.IFormFile)">
<summary>
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).
</summary>
<param name="userId">must be the recitation owner</param>
<param name="recitationId"></param>
<param name="xmlFile"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IRecitationService.GetPoemNarrationVerseSyncArray(System.Int32)"> <member name="M:RMuseum.Services.IRecitationService.GetPoemNarrationVerseSyncArray(System.Int32)">
<summary> <summary>
Gets Verse Sync Range Information Gets Verse Sync Range Information

View File

@ -73,6 +73,19 @@ namespace RMuseum.Services
/// <returns></returns> /// <returns></returns>
Task<RServiceResult<bool>> Delete(int id, Guid userId); Task<RServiceResult<bool>> Delete(int id, Guid userId);
/// <summary>
/// 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).
/// </summary>
/// <param name="userId">must be the recitation owner</param>
/// <param name="recitationId"></param>
/// <param name="xmlFile"></param>
/// <returns></returns>
Task<RServiceResult<bool>> ReplaceRecitationSyncXmlFile(Guid userId, int recitationId, IFormFile xmlFile);
/// <summary> /// <summary>
/// Gets Verse Sync Range Information /// Gets Verse Sync Range Information
/// </summary> /// </summary>

View File

@ -496,6 +496,161 @@ namespace RMuseum.Services.Implementationa
return new RServiceResult<bool>(true); return new RServiceResult<bool>(true);
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="userId">must be the recitation owner</param>
/// <param name="recitationId"></param>
/// <param name="xmlFile"></param>
/// <returns></returns>
public async Task<RServiceResult<bool>> ReplaceRecitationSyncXmlFile(Guid userId, int recitationId, IFormFile xmlFile)
{
if (xmlFile == null || xmlFile.Length == 0)
{
return new RServiceResult<bool>(false, "فایل xml ارسال نشده است.");
}
if (Path.GetExtension(xmlFile.FileName).ToLower() != ".xml")
{
return new RServiceResult<bool>(false, "تنها فایل با پسوند xml قابل پذیرش است.");
}
Recitation recitation = await _context.Recitations.Where(r => r.Id == recitationId && r.OwnerId == userId).FirstOrDefaultAsync();
if (recitation == null)
{
return new RServiceResult<bool>(false, "404");
}
if (recitation.ReviewStatus != AudioReviewStatus.Approved)
{
await _notificationService.PushNotification
(
userId,
"خطا در جایگزینی فایل xml",
$"جایگزینی فایل xml تنها برای خوانش‌های تأیید شده امکان‌پذیر است.{Environment.NewLine}" +
$"{recitation.AudioTitle}"
, NotificationType.Error
);
return new RServiceResult<bool>(false, "تنها امکان جایگزینی فایل xml برای خوانش‌های تأیید شده وجود دارد.");
}
RServiceResult<UploadSessionFile> savedFile = await SaveUploadedFile(xmlFile);
if (!string.IsNullOrEmpty(savedFile.ExceptionString))
{
return new RServiceResult<bool>(false, savedFile.ExceptionString);
}
string tempXmlFilePath = savedFile.Result.FilePath;
List<PoemAudio> 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<bool>(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<bool>(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<bool>(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<bool>(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<RMuseumDbContext>())) //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<bool>(true);
}
/// <summary> /// <summary>
/// Gets Verse Sync Range Information /// Gets Verse Sync Range Information