using System.Threading.Channels; using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.ProgramSchedules; public class UpdateProgramScheduleHandler( IDbContextFactory dbContextFactory, ChannelWriter channel) : IRequestHandler> { public async Task> Handle( UpdateProgramSchedule request, CancellationToken cancellationToken) { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeProgramSchedule = await ProgramScheduleMustExist(dbContext, request, cancellationToken); return await maybeProgramSchedule.Match( Some: async programSchedule => { Validation validation = await Validate(dbContext, request, programSchedule, cancellationToken); return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request, cancellationToken)); }, None: () => Task.FromResult>( new NotFoundError("Schedule does not exist"))); } private async Task ApplyUpdateRequest( TvContext dbContext, ProgramSchedule programSchedule, UpdateProgramSchedule request, CancellationToken cancellationToken) { // we need to refresh playouts if the playback order or keep multi-episodes has been modified bool needToRefreshPlayout = programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether || programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows || programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems || programSchedule.RandomStartPoint != request.RandomStartPoint || programSchedule.FixedStartTimeBehavior != request.FixedStartTimeBehavior; programSchedule.Name = request.Name; programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether && request.TreatCollectionsAsShows; programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems; programSchedule.RandomStartPoint = request.RandomStartPoint; programSchedule.FixedStartTimeBehavior = request.FixedStartTimeBehavior; // bump the optimistic-concurrency token so this config edit rotates other clients' ETags (#253). // Force-write past a concurrent Version bump (e.g. a parallel schedule-items replace) instead of // surfacing the IsConcurrencyToken guard as an unhandled DbUpdateConcurrencyException → 500 (#269). programSchedule.Version++; await dbContext.SaveChangesForcingVersion(cancellationToken); if (needToRefreshPlayout) { // post-commit side effect: run the affected-playout query + enqueue on CancellationToken.None // so a late request cancellation can't abort the rebuild after the edit already committed (#254) List playoutIds = await dbContext.Playouts .Filter(p => p.ProgramScheduleId == programSchedule.Id) .Map(p => p.Id) .ToListAsync(CancellationToken.None); foreach (int playoutId in playoutIds) { await channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh), CancellationToken.None); } } return new UpdateProgramScheduleResult(programSchedule.Id); } private static async Task> Validate( TvContext dbContext, UpdateProgramSchedule request, ProgramSchedule programSchedule, CancellationToken cancellationToken) => (await ValidateName(dbContext, request, cancellationToken)).Map(_ => programSchedule); private static Task> ProgramScheduleMustExist( TvContext dbContext, UpdateProgramSchedule request, CancellationToken cancellationToken) => dbContext.ProgramSchedules .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId, cancellationToken) .Map(identity); private static async Task> ValidateName( TvContext dbContext, UpdateProgramSchedule request, CancellationToken cancellationToken) { Validation result1 = request.NotEmpty(c => c.Name) .Bind(_ => request.NotLongerThan(50)(c => c.Name)); bool duplicateName = await dbContext.ProgramSchedules .AnyAsync(c => c.Id != request.ProgramScheduleId && c.Name == request.Name, cancellationToken); Validation result2 = duplicateName ? Fail("Schedule name must be unique") : Success(Unit.Default); return (result1, result2).Apply((_, _) => request.Name); } }