From e9a190312e44bf27c1a8505bfb8c2d8aba2dd284 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:00:21 -0500 Subject: [PATCH] fix: playout gap correctness in ui --- CHANGELOG.md | 2 ++ .../Playouts/Commands/BuildPlayoutHandler.cs | 9 +++++++-- .../Playouts/Commands/InsertPlayoutGaps.cs | 3 --- .../Scheduling/IPlayoutGapInserter.cs | 6 ++++++ .../Scheduling/PlayoutGapInserter.cs | 18 ++++++++++-------- .../Scheduling/PlayoutTimeShifter.cs | 10 ++++++++++ ErsatzTV/Services/WorkerService.cs | 3 --- ErsatzTV/Startup.cs | 1 + 8 files changed, 36 insertions(+), 16 deletions(-) delete mode 100644 ErsatzTV.Application/Playouts/Commands/InsertPlayoutGaps.cs create mode 100644 ErsatzTV.Core/Interfaces/Scheduling/IPlayoutGapInserter.cs rename ErsatzTV.Application/Playouts/Commands/InsertPlayoutGapsHandler.cs => ErsatzTV.Infrastructure/Scheduling/PlayoutGapInserter.cs (66%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5d04894..e90d74789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix regression from `v26.2.0` that caused channel logo watermarks to be ignored when the logo is a url - This affected external logo urls and generated channel logos - Maintain collection progress when refreshing a classic playout containing playlists +- Fix UI bug where on-demand channels would always show out of date gaps (unscheduled time) +- Fix UI bug where gaps (unscheduled time) would be shown from previous playout build, not current build ## [26.7.0] - 2026-07-27 ### Added diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index db445a3e7..91e49ed6a 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -26,6 +26,7 @@ public class BuildPlayoutHandler : IRequestHandler _workerChannel; private readonly ILogger _logger; @@ -43,6 +44,7 @@ public class BuildPlayoutHandler : IRequestHandler workerChannel, ILogger logger) { @@ -56,6 +58,7 @@ public class BuildPlayoutHandler : IRequestHandler 0) { _logger.LogDebug( @@ -305,8 +312,6 @@ public class BuildPlayoutHandler : IRequestHandler dbContextFactory) - : IRequestHandler +public class PlayoutGapInserter(IDbContextFactory dbContextFactory) : IPlayoutGapInserter { - public async Task Handle(InsertPlayoutGaps request, CancellationToken cancellationToken) + public async Task InsertGaps(int playoutId, CancellationToken cancellationToken) { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); var toAdd = new List(); IOrderedQueryable query = dbContext.PlayoutItems - .Filter(pi => pi.PlayoutId == request.PlayoutId) + .AsNoTracking() + .Filter(pi => pi.PlayoutId == playoutId) .OrderBy(i => i.Start); var queue = new Queue(query); @@ -27,14 +28,15 @@ public class InsertPlayoutGapsHandler(IDbContextFactory dbContextFact DateTime start = one.Finish; DateTime finish = two.Start; - if (start == finish) + // overlapping items would otherwise produce a negative-duration gap + if (start >= finish) { continue; } var gap = new PlayoutGap { - PlayoutId = request.PlayoutId, + PlayoutId = playoutId, Start = start, Finish = finish }; @@ -44,7 +46,7 @@ public class InsertPlayoutGapsHandler(IDbContextFactory dbContextFact // delete all existing gaps await dbContext.PlayoutGaps - .Where(pg => pg.PlayoutId == request.PlayoutId) + .Where(pg => pg.PlayoutId == playoutId) .ExecuteDeleteAsync(cancellationToken); // insert new gaps diff --git a/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs b/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs index 9e93f6ea8..ee1b635a0 100644 --- a/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs +++ b/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs @@ -30,6 +30,7 @@ public class PlayoutTimeShifter( Option maybePlayout = await dbContext.Playouts .Include(p => p.Channel) .Include(p => p.Items) + .Include(p => p.Gaps) .Include(p => p.Anchor) .Include(p => p.ProgramScheduleAnchors) .Include(p => p.PlayoutHistory) @@ -85,6 +86,8 @@ public class PlayoutTimeShifter( playout.Channel.Number, playout.Channel.Name); } + + playout.Gaps.RemoveAll(g => g.Finish < checkpointUtc); } TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now); @@ -119,6 +122,13 @@ public class PlayoutTimeShifter( } } + // time shift gaps along with the items they were calculated from + foreach (PlayoutGap gap in playout.Gaps) + { + gap.Start += toOffset; + gap.Finish += toOffset; + } + // time shift anchors foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors) { diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index 03d15d26d..80f05bf28 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -87,9 +87,6 @@ public class WorkerService : BackgroundService case CheckForOverlappingPlayoutItems checkForOverlappingPlayoutItems: await mediator.Send(checkForOverlappingPlayoutItems, stoppingToken); break; - case InsertPlayoutGaps insertPlayoutGaps: - await mediator.Send(insertPlayoutGaps, stoppingToken); - break; case TimeShiftOnDemandPlayout timeShiftOnDemandPlayout: await mediator.Send(timeShiftOnDemandPlayout, stoppingToken); break; diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 5fb9b272f..967239dcd 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -788,6 +788,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();