Files
ersatztv/ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs
T
timothyandClaude Opus 4.8 9b73b62527 feat(235): async-op API contract normalization — playouts slice C (#235)
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>
2026-07-11 17:58:15 +02:00

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);
}
}