diff --git a/CHANGELOG.md b/CHANGELOG.md index 77695b92..5294b45d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fix - Fix database operations that were slowing down playout builds - YAML playouts in particular should build significantly faster +- Fix channel playout mode `On Demand` for Block and YAML schedules ### Changed - Allow multiple watermarks in playback troubleshooting diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index 0af119a6..9c749f75 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -23,6 +23,7 @@ public class BuildPlayoutHandler : IRequestHandler _dbContextFactory; private readonly IEntityLocker _entityLocker; + private readonly IPlayoutTimeShifter _playoutTimeShifter; private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder; private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IPlayoutBuilder _playoutBuilder; @@ -39,6 +40,7 @@ public class BuildPlayoutHandler : IRequestHandler workerChannel) { _client = client; @@ -50,33 +52,60 @@ public class BuildPlayoutHandler : IRequestHandler> Handle(BuildPlayout request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Match( - playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken), - error => Task.FromResult>(error.Join())); + try + { + await _entityLocker.LockPlayout(request.PlayoutId); + if (request.Mode is not PlayoutBuildMode.Reset) + { + // this needs to happen before we load the playout in this handler because it modifies items, etc + await _playoutTimeShifter.TimeShift(request.PlayoutId, DateTimeOffset.Now, false); + } + + Either result; + + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + result = await validation.Match( + playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken), + error => Task.FromResult>(error.Join())); + } + + // after dbcontext is closed + foreach (var playoutBuildResult in result.RightToSeq()) + { + foreach (var timeShiftTo in playoutBuildResult.TimeShiftTo) + { + await _playoutTimeShifter.TimeShift(request.PlayoutId, timeShiftTo, false); + } + } + + return result.Map(_ => Unit.Default); + } + finally + { + await _entityLocker.UnlockPlayout(request.PlayoutId); + } } - private async Task> ApplyUpdateRequest( + private async Task> ApplyUpdateRequest( TvContext dbContext, BuildPlayout request, Playout playout, CancellationToken cancellationToken) { - string channelNumber; string channelName = "[unknown]"; try { - await _entityLocker.LockPlayout(playout.Id); - var referenceData = await GetReferenceData(dbContext, playout.Id, playout.ProgramSchedulePlayoutType); - channelNumber = referenceData.Channel.Number; + var channelNumber = referenceData.Channel.Number; channelName = referenceData.Channel.Name; var result = PlayoutBuildResult.Empty; @@ -207,6 +236,8 @@ public class BuildPlayoutHandler : IRequestHandler> Validate(TvContext dbContext, BuildPlayout request) => diff --git a/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs index b8c4d955..84089a8e 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs @@ -29,6 +29,10 @@ public class CreateBlockPlayoutHandler( await dbContext.Playouts.AddAsync(playout); await dbContext.SaveChangesAsync(); await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); + if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand) + { + await channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false)); + } await channel.WriteAsync(new RefreshChannelList()); return new CreatePlayoutResponse(playout.Id); } diff --git a/ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs index 5eb55e32..610bb3ab 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs @@ -39,7 +39,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler>, IBackgroundServiceRequest; diff --git a/ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs index 041b2988..98c747f2 100644 --- a/ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs @@ -1,41 +1,22 @@ using ErsatzTV.Core; -using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Scheduling; -using ErsatzTV.Infrastructure.Data; -using ErsatzTV.Infrastructure.Extensions; -using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Playouts; -public class TimeShiftOnDemandPlayoutHandler( - IPlayoutTimeShifter playoutTimeShifter, - IDbContextFactory dbContextFactory) +public class TimeShiftOnDemandPlayoutHandler(IPlayoutTimeShifter playoutTimeShifter) : IRequestHandler> { public async Task> Handle(TimeShiftOnDemandPlayout request, CancellationToken cancellationToken) { try { - await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); - - Option maybePlayout = await dbContext.Playouts - .Include(p => p.Channel) - .Include(p => p.Items) - .Include(p => p.Anchor) - .Include(p => p.ProgramScheduleAnchors) - .SelectOneAsync(p => p.Channel.Number, p => p.Channel.Number == request.ChannelNumber); - - foreach (Playout playout in maybePlayout) - { - playoutTimeShifter.TimeShift(playout, request.Now, request.Force); - await dbContext.SaveChangesAsync(cancellationToken); - } - - return Option.None; + await playoutTimeShifter.TimeShift(request.PlayoutId, request.Now, request.Force); } catch (Exception ex) { return BaseError.New(ex.Message); } + + return Option.None; } } diff --git a/ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs b/ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs index 0548b080..888e43ea 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs @@ -1,3 +1,4 @@ +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; @@ -29,6 +30,8 @@ public class UpdateOnDemandCheckpointHandler( return; } + playout.OnDemandCheckpoint ??= SystemTime.MinValueUtc; + int timeout = await (await configElementRepository.GetValue(ConfigElementKey.FFmpegSegmenterTimeout)) .IfNoneAsync(60); diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 884129be..26102642 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -183,11 +183,18 @@ public class HlsSessionWorker : IHlsSessionWorker PlaylistStart = _transcodedUntil; _channelStart = _transcodedUntil; - // time shift on-demand playout if needed - await _mediator.Send( - new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), + var maybePlayoutId = await _mediator.Send( + new GetPlayoutIdByChannelNumber(_channelNumber), cancellationToken); + // time shift on-demand playout if needed + foreach (var playoutId in maybePlayoutId) + { + await _mediator.Send( + new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true), + cancellationToken); + } + bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); _state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime; diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs b/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs index 2f76cc56..b857d3ce 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs @@ -130,11 +130,18 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker PlaylistStart = _transcodedUntil; _channelStart = _transcodedUntil; - // time shift on-demand playout if needed - await _mediator.Send( - new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), + var maybePlayoutId = await _mediator.Send( + new GetPlayoutIdByChannelNumber(_channelNumber), cancellationToken); + // time shift on-demand playout if needed + foreach (var playoutId in maybePlayoutId) + { + await _mediator.Send( + new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true), + cancellationToken); + } + // start concat/segmenter process // other transcode processes will be started by incoming requests from concat/segmenter process diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 4c1e0615..373bbea2 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -565,7 +565,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -664,7 +663,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -814,7 +812,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -923,7 +920,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1031,7 +1027,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1148,7 +1143,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1258,7 +1252,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1372,7 +1365,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1485,7 +1477,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1595,7 +1586,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1708,7 +1698,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1831,7 +1820,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -1947,7 +1935,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -2029,7 +2016,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -2247,7 +2233,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -2735,7 +2720,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -2846,7 +2830,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -2957,7 +2940,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, fakeRepository, televisionRepo, @@ -3066,7 +3048,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, collectionRepo, televisionRepo, @@ -3124,7 +3105,6 @@ public class PlayoutBuilderTests Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( - Substitute.For(), configRepo, collectionRepo, televisionRepo, diff --git a/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs b/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs index c5a7b848..8be6c49d 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs @@ -117,7 +117,6 @@ public class ScheduleIntegrationTests provider.GetRequiredService()); var builder = new PlayoutBuilder( - Substitute.For(), new ConfigElementRepository(factory), new MediaCollectionRepository(Substitute.For(), searchIndex, factory), new TelevisionRepository(factory, provider.GetRequiredService>()), @@ -297,7 +296,6 @@ public class ScheduleIntegrationTests DateTimeOffset finish = start.AddDays(2); var builder = new PlayoutBuilder( - Substitute.For(), new ConfigElementRepository(factory), new MediaCollectionRepository(Substitute.For(), Substitute.For(), factory), new TelevisionRepository(factory, provider.GetRequiredService>()), diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs b/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs index 78e8b6bc..30918eb6 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs @@ -1,8 +1,6 @@ -using ErsatzTV.Core.Domain; - namespace ErsatzTV.Core.Interfaces.Scheduling; public interface IPlayoutTimeShifter { - public void TimeShift(Playout playout, DateTimeOffset now, bool force); + public Task TimeShift(int playoutId, DateTimeOffset now, bool force); } diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs b/ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs index fc539ff4..740b71ab 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs @@ -10,8 +10,9 @@ public record PlayoutBuildResult( List AddedItems, List ItemsToRemove, List AddedHistory, - List HistoryToRemove) + List HistoryToRemove, + Option TimeShiftTo) { public static PlayoutBuildResult Empty => - new(false, Option.None, Option.None, [], [], [], []); + new(false, Option.None, Option.None, [], [], [], [], Option.None); } \ No newline at end of file diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index cc40168d..574d957b 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -22,13 +22,11 @@ public class PlayoutBuilder : IPlayoutBuilder private readonly ILocalFileSystem _localFileSystem; private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory; - private readonly IPlayoutTimeShifter _playoutTimeShifter; private readonly ITelevisionRepository _televisionRepository; private Playlist _debugPlaylist; private ILogger _logger; public PlayoutBuilder( - IPlayoutTimeShifter playoutTimeShifter, IConfigElementRepository configElementRepository, IMediaCollectionRepository mediaCollectionRepository, ITelevisionRepository televisionRepository, @@ -37,7 +35,6 @@ public class PlayoutBuilder : IPlayoutBuilder ILocalFileSystem localFileSystem, ILogger logger) { - _playoutTimeShifter = playoutTimeShifter; _configElementRepository = configElementRepository; _mediaCollectionRepository = mediaCollectionRepository; _televisionRepository = televisionRepository; @@ -89,12 +86,6 @@ public class PlayoutBuilder : IPlayoutBuilder // return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) }); // } - // time shift on demand channel if needed - if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand && mode is not PlayoutBuildMode.Reset) - { - _playoutTimeShifter.TimeShift(playout, parameters.Start, false); - } - result = await Build(playout, referenceData, result, mode, parameters, cancellationToken); } @@ -285,7 +276,7 @@ public class PlayoutBuilder : IPlayoutBuilder // time shift on demand channel if needed if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand) { - _playoutTimeShifter.TimeShift(playout, parameters.Start, false); + result = result with { TimeShiftTo = parameters.Start }; } return result; diff --git a/ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs b/ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs deleted file mode 100644 index c1381cd8..00000000 --- a/ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.FFmpeg; -using ErsatzTV.Core.Interfaces.Scheduling; -using Microsoft.Extensions.Logging; - -namespace ErsatzTV.Core.Scheduling; - -public class PlayoutTimeShifter(IFFmpegSegmenterService segmenterService, ILogger logger) - : IPlayoutTimeShifter -{ - public void TimeShift(Playout playout, DateTimeOffset now, bool force) - { - if (playout.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand) - { - return; - } - - if (!force && segmenterService.IsActive(playout.Channel.Number)) - { - logger.LogDebug( - "Will not time shift on demand playout that is active for channel {Number} - {Name}", - playout.Channel.Number, - playout.Channel.Name); - - return; - } - - if (playout.Items.Count == 0) - { - logger.LogDebug( - "Unable to time shift empty playout for channel {Number} - {Name}", - playout.Channel.Number, - playout.Channel.Name); - - return; - } - - if (playout.OnDemandCheckpoint is null) - { - logger.LogDebug( - "Time shifting unwatched playout for channel {Number} - {Name}", - playout.Channel.Number, - playout.Channel.Name); - - playout.OnDemandCheckpoint = playout.Items.Min(p => p.StartOffset); - } - - TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now); - - logger.LogDebug( - "Time shifting playout for channel {Number} - {Name} forward by {Time}", - playout.Channel.Number, - playout.Channel.Name, - toOffset); - - // time shift items - foreach (PlayoutItem playoutItem in playout.Items) - { - playoutItem.Start += toOffset; - playoutItem.Finish += toOffset; - - if (playoutItem.GuideStart.HasValue) - { - playoutItem.GuideStart += toOffset; - } - - if (playoutItem.GuideFinish.HasValue) - { - playoutItem.GuideFinish += toOffset; - } - } - - // time shift anchors - foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors) - { - if (anchor.AnchorDate.HasValue) - { - anchor.AnchorDate += toOffset; - } - } - - // time shift anchor - if (playout.Anchor is not null) - { - playout.Anchor.NextStart += toOffset; - } - - playout.OnDemandCheckpoint = now; - } -} diff --git a/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs b/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs new file mode 100644 index 00000000..6649c010 --- /dev/null +++ b/ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs @@ -0,0 +1,128 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Interfaces.FFmpeg; +using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Infrastructure.Scheduling; + +public class PlayoutTimeShifter( + IDbContextFactory dbContextFactory, + IFFmpegSegmenterService segmenterService, + ILogger logger) + : IPlayoutTimeShifter +{ + public async Task TimeShift(int playoutId, DateTimeOffset now, bool force) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); + + Option maybeChannel = await dbContext.Playouts + .AsNoTracking() + .Include(p => p.Channel) + .SelectOneAsync(p => p.Id, p => p.Id == playoutId) + .MapT(p => p.Channel); + + foreach (var channel in maybeChannel.Where(c => c.PlayoutMode is ChannelPlayoutMode.OnDemand)) + { + Option maybePlayout = await dbContext.Playouts + .Include(p => p.Channel) + .Include(p => p.Items) + .Include(p => p.Anchor) + .Include(p => p.ProgramScheduleAnchors) + .Include(p => p.PlayoutHistory) + .SelectOneAsync(p => p.ChannelId, p => p.ChannelId == channel.Id); + + foreach (var playout in maybePlayout) + { + if (playout.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand) + { + return; + } + + if (!force && segmenterService.IsActive(playout.Channel.Number)) + { + logger.LogDebug( + "Will not time shift on demand playout that is active for channel {Number} - {Name}", + playout.Channel.Number, + playout.Channel.Name); + + return; + } + + if (playout.Items.Count == 0) + { + logger.LogDebug( + "Unable to time shift empty playout for channel {Number} - {Name}", + playout.Channel.Number, + playout.Channel.Name); + + return; + } + + if (playout.OnDemandCheckpoint is null) + { + logger.LogDebug( + "Time shifting unwatched playout for channel {Number} - {Name}", + playout.Channel.Number, + playout.Channel.Name); + + playout.OnDemandCheckpoint = playout.Items.Min(p => p.StartOffset); + } + + TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now); + + logger.LogDebug( + "Time shifting playout for channel {Number} - {Name} forward by {Time}", + playout.Channel.Number, + playout.Channel.Name, + toOffset); + + // time shift history + foreach (PlayoutHistory history in playout.PlayoutHistory) + { + history.When += toOffset; + history.Finish += toOffset; + } + + // time shift items + foreach (PlayoutItem playoutItem in playout.Items) + { + playoutItem.Start += toOffset; + playoutItem.Finish += toOffset; + + if (playoutItem.GuideStart.HasValue) + { + playoutItem.GuideStart += toOffset; + } + + if (playoutItem.GuideFinish.HasValue) + { + playoutItem.GuideFinish += toOffset; + } + } + + // time shift anchors + foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors) + { + if (anchor.AnchorDate.HasValue) + { + anchor.AnchorDate += toOffset; + } + } + + // time shift anchor + if (playout.Anchor is not null) + { + playout.Anchor.NextStart += toOffset; + } + + playout.OnDemandCheckpoint = now; + + await dbContext.SaveChangesAsync(); + } + } + } +}