using System.CommandLine.Parsing; using System.IO.Abstractions; using System.Threading.Channels; using ErsatzTV.Application.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Playouts; public class UpdateScriptedPlayoutHandler( IDbContextFactory dbContextFactory, ChannelWriter workerChannel, IFileSystem fileSystem) : IRequestHandler> { public async Task> Handle( UpdateScriptedPlayout request, CancellationToken cancellationToken) { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request, cancellationToken); return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken)); } private async Task ApplyUpdateRequest( TvContext dbContext, UpdateScriptedPlayout request, Playout playout, CancellationToken cancellationToken) { playout.ScheduleFile = request.ScheduleFile; // Rotate the playout ETag only when the schedule-file actually changed — a no-op re-submit must // not bump (#269). When it changed, force-write past a concurrent Version bump from a replace-all // editor (this writer takes no If-Match, so a benign race must not 500) — #253/#269 §7a. if (dbContext.ChangeTracker.HasChanges()) { playout.Version++; await dbContext.SaveChangesForcingVersion(cancellationToken); // post-commit side effect runs on CancellationToken.None so a late request cancellation // can't abort it after the commit landed (#254) await workerChannel.WriteAsync(new RefreshChannelData(playout.Channel.Number), CancellationToken.None); } return new PlayoutNameViewModel( playout.Id, playout.ScheduleKind, playout.Channel.Name, playout.Channel.Number, playout.Channel.Id, playout.Channel.PlayoutMode, playout.ProgramSchedule?.Name ?? string.Empty, playout.ScheduleFile, playout.DailyRebuildTime, playout.BuildStatus, playout.DecoId, playout.Deco?.Name, playout.Version, playout.Seed); } private async Task> Validate( TvContext dbContext, UpdateScriptedPlayout request, CancellationToken cancellationToken) => (ValidateScheduleFile(request), await PlayoutMustExist(dbContext, request, cancellationToken)) .Apply((_, playout) => playout); private Validation ValidateScheduleFile(UpdateScriptedPlayout request) { var args = CommandLineParser.SplitCommandLine(request.ScheduleFile).ToList(); string scriptFile = args[0]; if (!fileSystem.File.Exists(scriptFile)) { return BaseError.New("Scripted schedule does not exist!"); } return request.ScheduleFile; } private static Task> PlayoutMustExist( TvContext dbContext, UpdateScriptedPlayout updatePlayout, CancellationToken cancellationToken) => dbContext.Playouts .Include(p => p.Channel) .SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId, cancellationToken) .Map(o => o.ToValidation("Playout does not exist.")); }