Implement media retry functionality and enhance error handling
Added endpoints for retrying failed media processing, allowing users to requeue media assets that encountered errors. Introduced error messages for scenarios where a media asset cannot be retried due to its status. Updated the MaintenanceBackgroundService to remove orphaned episodes and recompute group statistics, ensuring data integrity. Enhanced the frontend to support retry actions, including bulk retry options for failed media. Updated localization strings to reflect new features in both English and Russian.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media;
|
||||
using TeleWave.Application.Media.Delete;
|
||||
using TeleWave.Application.Media.Retry;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Application.Tests.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Перезапуск упавшей обработки. Файл при этом на месте — упала только нарезка, и повторить её
|
||||
/// обычно и есть всё лечение; до этого оставалось удалить ассет и залить заново.
|
||||
/// </summary>
|
||||
public class RetryMediaTests
|
||||
{
|
||||
private static MediaAsset Failed(string name)
|
||||
{
|
||||
var asset = MediaAsset.Register(name, ".mkv", MediaSource.Upload);
|
||||
asset.MarkProcessing();
|
||||
asset.MarkFailed("Процесс /usr/bin/ffmpeg превысил таймаут");
|
||||
return asset;
|
||||
}
|
||||
|
||||
private static MediaAsset Ready(string name)
|
||||
{
|
||||
var asset = MediaAsset.Register(name, ".mkv", MediaSource.Upload);
|
||||
asset.MarkProcessing();
|
||||
asset.MarkReady(
|
||||
new MediaReadyInfo(TimeSpan.FromMinutes(30), 2, 900, 1920, 1080, "h264", "aac", "x")
|
||||
);
|
||||
return asset;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Retry_ReturnsFailedAssetToTheQueue()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var failed = Failed("Гладиатор.mkv");
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(failed);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var queue = Substitute.For<IMediaProcessingQueue>();
|
||||
await using var db = fixture.New();
|
||||
var result = await new RetryMediaCommandHandler(db, queue).Handle(
|
||||
new RetryMediaCommand(failed.Id),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(1, result.Value.Requeued);
|
||||
// Обработчик будят после сохранения: до коммита он прочитал бы из базы ещё Failed.
|
||||
queue.Received(1).Enqueue(failed.Id);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = verify.MediaAssets.Single(a => a.Id == failed.Id);
|
||||
Assert.Equal(MediaAssetStatus.Pending, stored.Status);
|
||||
Assert.Null(stored.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Retry_WithoutId_TakesEveryFailedButLeavesTheRest()
|
||||
{
|
||||
// Падают пачкой и по одной причине, чинят тоже пачкой — но готовое трогать нельзя:
|
||||
// оно уже стоит в эфире.
|
||||
var fixture = new TestDb();
|
||||
var first = Failed("Терминатор.mkv");
|
||||
var second = Failed("Криминальное чтиво.mkv");
|
||||
var ready = Ready("Симпсоны s01e01.mkv");
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.AddRange(first, second, ready);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var queue = Substitute.For<IMediaProcessingQueue>();
|
||||
await using var db = fixture.New();
|
||||
var result = await new RetryMediaCommandHandler(db, queue).Handle(
|
||||
new RetryMediaCommand(),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Equal(2, result.Value.Requeued);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
Assert.Equal(
|
||||
MediaAssetStatus.Ready,
|
||||
verify.MediaAssets.Single(a => a.Id == ready.Id).Status
|
||||
);
|
||||
Assert.All(
|
||||
verify.MediaAssets.Where(a => a.Id != ready.Id).ToList(),
|
||||
a => Assert.Equal(MediaAssetStatus.Pending, a.Status)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_AlsoRemovesTheEpisodeFromTheShow()
|
||||
{
|
||||
// Серия ссылается на ассет без внешнего ключа: без этого шага в шоу остаётся строка,
|
||||
// за которой нет файла, — в эфир она не идёт и ничем себя не выдаёт.
|
||||
var fixture = new TestDb();
|
||||
var asset = Ready("Металлопокалипсис s01e38.mkv");
|
||||
var show = Show.Create("Металлопокалипсис", ShowKind.Series);
|
||||
show.AddEpisode(asset.Id);
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(asset);
|
||||
seed.Shows.Add(show);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new DeleteMediaAssetCommandHandler(
|
||||
db,
|
||||
Substitute.For<IMediaStorage>()
|
||||
).Handle(new DeleteMediaAssetCommand(asset.Id), CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await using var verify = fixture.New();
|
||||
var stored = verify.Shows.Include(s => s.Episodes).Single(s => s.Id == show.Id);
|
||||
Assert.Empty(stored.Episodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Retry_OfHealthyAsset_IsRefused()
|
||||
{
|
||||
var fixture = new TestDb();
|
||||
var ready = Ready("Симпсоны s01e02.mkv");
|
||||
|
||||
await using (var seed = fixture.New())
|
||||
{
|
||||
seed.MediaAssets.Add(ready);
|
||||
await seed.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
await using var db = fixture.New();
|
||||
var result = await new RetryMediaCommandHandler(
|
||||
db,
|
||||
Substitute.For<IMediaProcessingQueue>()
|
||||
).Handle(new RetryMediaCommand(ready.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal(MediaErrors.NotFailed, result.Error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user