Merge branch 'feat/235-s3-playouts' into feat/235-async-contract

This commit is contained in:
2026-07-11 18:02:58 +02:00
11 changed files with 188 additions and 29 deletions
@@ -211,7 +211,7 @@ public class ChannelController(
"progress) and all other playout kinds use Reset (rebuild from scratch), matching the Blazor UI. " +
"Pass mode to force a specific PlayoutBuildMode.")]
[EndpointGroupName("general")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<IActionResult> ResetPlayout(
@@ -233,7 +233,7 @@ public class ChannelController(
PlayoutBuildMode buildMode = mode ?? await DefaultResetMode(playoutId, cancellationToken);
await workerChannel.WriteAsync(new BuildPlayout(playoutId, buildMode), cancellationToken);
return new OkResult();
return new AcceptedResult();
}
return ApiResults.NotFoundProblem();
+19 -10
View File
@@ -69,7 +69,7 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
Option<PlayoutNameViewModel> result = await mediator.Send(new GetPlayoutById(id), cancellationToken);
return result.Map(ToResponse).ToGetResult();
return result.Map(vm => ToResponse(vm, entityLocker.IsPlayoutLocked(id))).ToGetResult();
}
[HttpGet("/api/playouts/{id:int}/items", Name = "GetPlayoutItems")]
@@ -128,7 +128,9 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
Option<PlayoutNameViewModel> playout =
await mediator.Send(new GetPlayoutById(created.PlayoutId), cancellationToken);
return playout.Match(
Some: vm => (IActionResult)new CreatedResult($"/api/playouts/{vm.PlayoutId}", ToResponse(vm)),
Some: vm => (IActionResult)new CreatedResult(
$"/api/playouts/{vm.PlayoutId}",
ToResponse(vm, entityLocker.IsPlayoutLocked(vm.PlayoutId))),
None: () => ApiResults.NotFoundProblem());
});
});
@@ -194,7 +196,8 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
return result.Match(
Left: error => error.ToErrorResult(),
Right: playout => (IActionResult)new OkObjectResult(ToResponse(playout)));
Right: playout => (IActionResult)new OkObjectResult(
ToResponse(playout, entityLocker.IsPlayoutLocked(id))));
}
private async Task<Either<BaseError, PlayoutNameViewModel>> UpdateScheduleFile(
@@ -260,7 +263,7 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
Option<PlayoutNameViewModel> refreshed = await mediator.Send(new GetPlayoutById(id), cancellationToken);
return refreshed.Match(
Some: vm => (IActionResult)new OkObjectResult(ToResponse(vm)),
Some: vm => (IActionResult)new OkObjectResult(ToResponse(vm, entityLocker.IsPlayoutLocked(id))),
None: () => ApiResults.NotFoundProblem());
}
@@ -550,14 +553,19 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
[Tags("Playouts")]
[EndpointSummary("Reset all playouts")]
[EndpointGroupName("general")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(typeof(ResetAllPlayoutsResponseModel), StatusCodes.Status202Accepted)]
// No 409 lock guard here (unlike the id-keyed mutations): ResetAllPlayoutsHandler already
// skips any playout whose build lock is held, matching Blazor. It is a fire-and-forget
// bulk enqueue, so it always accepts. See docs/decisions.md 2026-07-10.
// bulk enqueue, so it always accepts — the 202 body reports which playouts were queued and
// which were skipped (locked, or an unsupported ExternalJson/None kind). See docs/decisions.md 2026-07-10.
public async Task<IActionResult> ResetAll(CancellationToken cancellationToken)
{
await mediator.Send(new ResetAllPlayouts(), cancellationToken);
return Accepted();
ResetAllPlayoutsResult result = await mediator.Send(new ResetAllPlayouts(), cancellationToken);
var body = new ResetAllPlayoutsResponseModel(
result.QueuedPlayoutIds,
result.SkippedLocked,
result.SkippedUnsupported);
return new AcceptedResult((string)null, body);
}
[HttpPost("/api/playouts/{id:int}/erase-items", Name = "ErasePlayoutItems")]
@@ -728,7 +736,7 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
private static PlayoutHistoryDetailsResponseModel ToDetailsResponse(PlayoutHistoryDetailsViewModel vm) =>
new(vm.PlaybackOrder, vm.CollectionType, vm.Name, vm.MediaItemType, vm.MediaItemTitle);
private static PlayoutResponseModel ToResponse(PlayoutNameViewModel vm) =>
private static PlayoutResponseModel ToResponse(PlayoutNameViewModel vm, bool isLocked) =>
PlayoutResponseModel.From(
vm.PlayoutId,
vm.ScheduleKind,
@@ -740,7 +748,8 @@ public class PlayoutController(IMediator mediator, IEntityLocker entityLocker) :
vm.DbDailyRebuildTime,
ToBuildStatus(vm.BuildStatus),
vm.DecoId,
vm.DecoName);
vm.DecoName,
isLocked);
private static PlayoutAlternateScheduleResponseModel ToResponse(PlayoutAlternateScheduleViewModel vm) =>
new(