Slice C of the async-op contract normalization:
- channel reset (POST /api/channels/{channelNumber}/playout/reset) now
returns 202 Accepted (was 200 Ok) — it only queues a background rebuild
- reset-all (POST /api/playouts/reset-all) still 202 but now returns a
ResetAllPlayoutsResponseModel body reporting QueuedPlayoutIds /
SkippedLocked / SkippedUnsupported instead of silently swallowing skips;
handler returns a new ResetAllPlayoutsResult record
- single-playout GET (GET /api/playouts/{id}) now exposes IsLocked on
PlayoutResponseModel, set from IEntityLocker.IsPlayoutLocked mirroring
the list projection — gives a polling client the lock flag
Tests: channel reset asserts 202; reset-all asserts 202 + skipped-body
shape; single GET asserts IsLocked; new ResetAllPlayoutsHandlerTests
(in-memory SQLite) asserts locked/ExternalJson/None land in skipped lists
and eligible playouts in queued. docs/api-conventions.md §3a updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.5 KiB
C#
72 lines
2.5 KiB
C#
using System.Threading.Channels;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Interfaces.Locking;
|
|
using ErsatzTV.Core.Scheduling;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ErsatzTV.Application.Playouts;
|
|
|
|
public class ResetAllPlayoutsHandler(
|
|
IEntityLocker locker,
|
|
ChannelWriter<IBackgroundServiceRequest> channel,
|
|
IDbContextFactory<TvContext> dbContextFactory)
|
|
: IRequestHandler<ResetAllPlayouts, ResetAllPlayoutsResult>
|
|
{
|
|
public async Task<ResetAllPlayoutsResult> Handle(
|
|
ResetAllPlayouts request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
|
|
var queued = new List<int>();
|
|
var skippedLocked = new List<int>();
|
|
var skippedUnsupported = new List<int>();
|
|
|
|
foreach (Playout playout in await dbContext.Playouts.ToListAsync(cancellationToken))
|
|
{
|
|
switch (playout.ScheduleKind)
|
|
{
|
|
case PlayoutScheduleKind.Classic:
|
|
if (locker.IsPlayoutLocked(playout.Id))
|
|
{
|
|
skippedLocked.Add(playout.Id);
|
|
}
|
|
else
|
|
{
|
|
await channel.WriteAsync(
|
|
new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh),
|
|
cancellationToken);
|
|
queued.Add(playout.Id);
|
|
}
|
|
|
|
break;
|
|
case PlayoutScheduleKind.Block:
|
|
case PlayoutScheduleKind.Sequential:
|
|
case PlayoutScheduleKind.Scripted:
|
|
if (locker.IsPlayoutLocked(playout.Id))
|
|
{
|
|
skippedLocked.Add(playout.Id);
|
|
}
|
|
else
|
|
{
|
|
await channel.WriteAsync(
|
|
new BuildPlayout(playout.Id, PlayoutBuildMode.Reset),
|
|
cancellationToken);
|
|
queued.Add(playout.Id);
|
|
}
|
|
|
|
break;
|
|
case PlayoutScheduleKind.ExternalJson:
|
|
case PlayoutScheduleKind.None:
|
|
default:
|
|
// external json cannot be reset
|
|
skippedUnsupported.Add(playout.Id);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return new ResetAllPlayoutsResult(queued, skippedLocked, skippedUnsupported);
|
|
}
|
|
}
|