Browse Source

fix: playout gap correctness in ui (#2968)

pull/2969/head
Jason Dove 1 day ago committed by GitHub
parent
commit
3d24c3f0f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 9
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 3
      ErsatzTV.Application/Playouts/Commands/InsertPlayoutGaps.cs
  4. 6
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutGapInserter.cs
  5. 18
      ErsatzTV.Infrastructure/Scheduling/PlayoutGapInserter.cs
  6. 10
      ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs
  7. 3
      ErsatzTV/Services/WorkerService.cs
  8. 1
      ErsatzTV/Startup.cs

2
CHANGELOG.md

@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

9
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -26,6 +26,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -26,6 +26,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder;
private readonly IPlayoutGapInserter _playoutGapInserter;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly ILogger<BuildPlayoutHandler> _logger;
@ -43,6 +44,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -43,6 +44,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
IPlayoutTimeShifter playoutTimeShifter,
IPlayoutGapInserter playoutGapInserter,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ILogger<BuildPlayoutHandler> logger)
{
@ -56,6 +58,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -56,6 +58,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
_playoutTimeShifter = playoutTimeShifter;
_playoutGapInserter = playoutGapInserter;
_workerChannel = workerChannel;
_logger = logger;
}
@ -89,6 +92,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -89,6 +92,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
await _playoutTimeShifter.TimeShift(request.PlayoutId, timeShiftTo, false, cancellationToken);
}
// must happen after any time shift, and before the playout is unlocked so that
// the ui sees items and gaps from the same build
await _playoutGapInserter.InsertGaps(request.PlayoutId, cancellationToken);
if (playoutBuildResult.Warnings.TailFillerTooLong > 0)
{
_logger.LogDebug(
@ -305,8 +312,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -305,8 +312,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
new CheckForOverlappingPlayoutItems(request.PlayoutId),
cancellationToken);
await _workerChannel.WriteAsync(new InsertPlayoutGaps(request.PlayoutId), cancellationToken);
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName) ||
playout.ScheduleKind is PlayoutScheduleKind.ExternalJson)

3
ErsatzTV.Application/Playouts/Commands/InsertPlayoutGaps.cs

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
namespace ErsatzTV.Application.Playouts;
public record InsertPlayoutGaps(int PlayoutId) : IRequest, IBackgroundServiceRequest;

6
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutGapInserter.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutGapInserter
{
Task InsertGaps(int playoutId, CancellationToken cancellationToken);
}

18
ErsatzTV.Application/Playouts/Commands/InsertPlayoutGapsHandler.cs → ErsatzTV.Infrastructure/Scheduling/PlayoutGapInserter.cs

@ -1,21 +1,22 @@ @@ -1,21 +1,22 @@
using EFCore.BulkExtensions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
namespace ErsatzTV.Infrastructure.Scheduling;
public class InsertPlayoutGapsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<InsertPlayoutGaps>
public class PlayoutGapInserter(IDbContextFactory<TvContext> 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<PlayoutGap>();
IOrderedQueryable<PlayoutItem> query = dbContext.PlayoutItems
.Filter(pi => pi.PlayoutId == request.PlayoutId)
.AsNoTracking()
.Filter(pi => pi.PlayoutId == playoutId)
.OrderBy(i => i.Start);
var queue = new Queue<PlayoutItem>(query);
@ -27,14 +28,15 @@ public class InsertPlayoutGapsHandler(IDbContextFactory<TvContext> dbContextFact @@ -27,14 +28,15 @@ public class InsertPlayoutGapsHandler(IDbContextFactory<TvContext> 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<TvContext> dbContextFact @@ -44,7 +46,7 @@ public class InsertPlayoutGapsHandler(IDbContextFactory<TvContext> dbContextFact
// delete all existing gaps
await dbContext.PlayoutGaps
.Where(pg => pg.PlayoutId == request.PlayoutId)
.Where(pg => pg.PlayoutId == playoutId)
.ExecuteDeleteAsync(cancellationToken);
// insert new gaps

10
ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs

@ -30,6 +30,7 @@ public class PlayoutTimeShifter( @@ -30,6 +30,7 @@ public class PlayoutTimeShifter(
Option<Playout> 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( @@ -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( @@ -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)
{

3
ErsatzTV/Services/WorkerService.cs

@ -87,9 +87,6 @@ public class WorkerService : BackgroundService @@ -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;

1
ErsatzTV/Startup.cs

@ -788,6 +788,7 @@ public class Startup @@ -788,6 +788,7 @@ public class Startup
services.AddScoped<ISchedulingEngine, SchedulingEngine>();
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
services.AddScoped<IPlayoutGapInserter, PlayoutGapInserter>();
services.AddScoped<IImageCache, ImageCache>();
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();

Loading…
Cancel
Save