Browse Source

refactor playout build errors (#2480)

* refactor classic playout builds

* refactor sequential playout builds

* refactor block playout building

* don't fail building an empty block schedule

* fix scripted playout build errors
pull/2483/head
Jason Dove 8 months ago committed by GitHub
parent
commit
3e8ac9914c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 102
      ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs
  2. 238
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 72
      ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs
  4. 42
      ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs
  5. 206
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs
  6. 690
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs
  7. 33
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs
  8. 18
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ResetPlayoutTests.cs
  9. 2
      ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs
  10. 2
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs
  11. 2
      ErsatzTV.Core/Interfaces/Scheduling/IScriptedPlayoutBuilder.cs
  12. 2
      ErsatzTV.Core/Interfaces/Scheduling/ISequentialPlayoutBuilder.cs
  13. 7
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  14. 11
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs
  15. 8
      ErsatzTV.Core/Scheduling/PlayoutBuildException.cs
  16. 132
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  17. 10
      ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs
  18. 14
      ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs

102
ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Application.Scheduling; using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -57,65 +58,70 @@ public class PreviewPlaylistPlayoutHandler(
// TODO: make an explicit method to preview, this is ugly // TODO: make an explicit method to preview, this is ugly
playoutBuilder.TrimStart = false; playoutBuilder.TrimStart = false;
playoutBuilder.DebugPlaylist = playout.ProgramSchedule.Items[0].Playlist; playoutBuilder.DebugPlaylist = playout.ProgramSchedule.Items[0].Playlist;
PlayoutBuildResult result = await playoutBuilder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await playoutBuilder.Build(
DateTimeOffset.Now, DateTimeOffset.Now,
playout, playout,
referenceData, referenceData,
PlayoutBuildMode.Reset, PlayoutBuildMode.Reset,
cancellationToken); cancellationToken);
var maxItems = 0; return await buildResult.MatchAsync(
Dictionary<PlaylistItem, List<MediaItem>> map = async result =>
await mediaCollectionRepository.GetPlaylistItemMap(
playout.ProgramSchedule.Items[0].Playlist,
cancellationToken);
foreach (PlaylistItem item in playout.ProgramSchedule.Items[0].Playlist.Items)
{
if (item.PlayAll)
{
maxItems += map[item].Count;
}
else
{ {
maxItems += 1; var maxItems = 0;
} Dictionary<PlaylistItem, List<MediaItem>> map =
} await mediaCollectionRepository.GetPlaylistItemMap(
playout.ProgramSchedule.Items[0].Playlist,
cancellationToken);
foreach (PlaylistItem item in playout.ProgramSchedule.Items[0].Playlist.Items)
{
if (item.PlayAll)
{
maxItems += map[item].Count;
}
else
{
maxItems += 1;
}
}
// limit preview to once through the playlist // limit preview to once through the playlist
var onceThrough = result.AddedItems.Take(maxItems).ToList(); var onceThrough = result.AddedItems.Take(maxItems).ToList();
// load playout item details for title // load playout item details for title
foreach (PlayoutItem playoutItem in onceThrough) foreach (PlayoutItem playoutItem in onceThrough)
{ {
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking() .AsNoTracking()
.Include(mi => (mi as Movie).MovieMetadata) .Include(mi => (mi as Movie).MovieMetadata)
.Include(mi => (mi as Movie).MediaVersions) .Include(mi => (mi as Movie).MediaVersions)
.Include(mi => (mi as MusicVideo).MusicVideoMetadata) .Include(mi => (mi as MusicVideo).MusicVideoMetadata)
.Include(mi => (mi as MusicVideo).MediaVersions) .Include(mi => (mi as MusicVideo).MediaVersions)
.Include(mi => (mi as MusicVideo).Artist) .Include(mi => (mi as MusicVideo).Artist)
.ThenInclude(mm => mm.ArtistMetadata) .ThenInclude(mm => mm.ArtistMetadata)
.Include(mi => (mi as Episode).EpisodeMetadata) .Include(mi => (mi as Episode).EpisodeMetadata)
.Include(mi => (mi as Episode).MediaVersions) .Include(mi => (mi as Episode).MediaVersions)
.Include(mi => (mi as Episode).Season) .Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata) .ThenInclude(s => s.SeasonMetadata)
.Include(mi => (mi as Episode).Season.Show) .Include(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
.Include(mi => (mi as OtherVideo).OtherVideoMetadata) .Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.Include(mi => (mi as OtherVideo).MediaVersions) .Include(mi => (mi as OtherVideo).MediaVersions)
.Include(mi => (mi as Song).SongMetadata) .Include(mi => (mi as Song).SongMetadata)
.Include(mi => (mi as Song).MediaVersions) .Include(mi => (mi as Song).MediaVersions)
.Include(mi => (mi as Image).ImageMetadata) .Include(mi => (mi as Image).ImageMetadata)
.Include(mi => (mi as Image).MediaVersions) .Include(mi => (mi as Image).MediaVersions)
.SelectOneAsync(mi => mi.Id, mi => mi.Id == playoutItem.MediaItemId, cancellationToken); .SelectOneAsync(mi => mi.Id, mi => mi.Id == playoutItem.MediaItemId, cancellationToken);
foreach (MediaItem mediaItem in maybeMediaItem) foreach (MediaItem mediaItem in maybeMediaItem)
{ {
playoutItem.MediaItem = mediaItem; playoutItem.MediaItem = mediaItem;
} }
} }
return onceThrough.OrderBy(i => i.StartOffset).Map(Scheduling.Mapper.ProjectToViewModel).ToList(); return onceThrough.OrderBy(i => i.StartOffset).Map(Scheduling.Mapper.ProjectToViewModel).ToList();
},
_ => []);
} }
private static ProgramScheduleItemFlood MapToScheduleItem(PreviewPlaylistPlayout request) => private static ProgramScheduleItemFlood MapToScheduleItem(PreviewPlaylistPlayout request) =>

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

@ -124,26 +124,31 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
playout.ScheduleKind); playout.ScheduleKind);
string channelNumber = referenceData.Channel.Number; string channelNumber = referenceData.Channel.Number;
channelName = referenceData.Channel.Name; channelName = referenceData.Channel.Name;
PlayoutBuildResult result = PlayoutBuildResult.Empty; Either<BaseError, PlayoutBuildResult> buildResult = BaseError.New("Unsupported schedule kind");
switch (playout.ScheduleKind) switch (playout.ScheduleKind)
{ {
case PlayoutScheduleKind.Block: case PlayoutScheduleKind.Block:
result = await _blockPlayoutBuilder.Build( buildResult = await _blockPlayoutBuilder.Build(
request.Start, request.Start,
playout, playout,
referenceData, referenceData,
request.Mode, request.Mode,
cancellationToken); cancellationToken);
result = await _blockPlayoutFillerBuilder.Build(
playout, foreach (var result in buildResult.RightToSeq())
referenceData, {
result, buildResult = await _blockPlayoutFillerBuilder.Build(
request.Mode, playout,
cancellationToken); referenceData,
result,
request.Mode,
cancellationToken);
}
break; break;
case PlayoutScheduleKind.Sequential: case PlayoutScheduleKind.Sequential:
result = await _sequentialPlayoutBuilder.Build( buildResult = await _sequentialPlayoutBuilder.Build(
request.Start, request.Start,
playout, playout,
referenceData, referenceData,
@ -151,7 +156,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
cancellationToken); cancellationToken);
break; break;
case PlayoutScheduleKind.Scripted: case PlayoutScheduleKind.Scripted:
result = await _scriptedPlayoutBuilder.Build( buildResult = await _scriptedPlayoutBuilder.Build(
request.Start, request.Start,
playout, playout,
referenceData, referenceData,
@ -164,7 +169,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
case PlayoutScheduleKind.None: case PlayoutScheduleKind.None:
case PlayoutScheduleKind.Classic: case PlayoutScheduleKind.Classic:
default: default:
result = await _playoutBuilder.Build( buildResult = await _playoutBuilder.Build(
request.Start, request.Start,
playout, playout,
referenceData, referenceData,
@ -173,123 +178,134 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
break; break;
} }
var changeCount = 0; return await buildResult.MatchAsync<Either<BaseError, PlayoutBuildResult>>(
async result =>
{
var changeCount = 0;
if (result.RerunHistoryToRemove.Count > 0) if (result.RerunHistoryToRemove.Count > 0)
{ {
changeCount += await dbContext.RerunHistory changeCount += await dbContext.RerunHistory
.Where(rh => result.RerunHistoryToRemove.Contains(rh.Id)) .Where(rh => result.RerunHistoryToRemove.Contains(rh.Id))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
if (result.AddedRerunHistory.Count > 0) if (result.AddedRerunHistory.Count > 0)
{ {
changeCount += 1; changeCount += 1;
await dbContext.BulkInsertAsync(result.AddedRerunHistory, cancellationToken: cancellationToken); await dbContext.BulkInsertAsync(result.AddedRerunHistory, cancellationToken: cancellationToken);
} }
if (result.ClearItems) if (result.ClearItems)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id) .Where(pi => pi.PlayoutId == playout.Id)
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
foreach (DateTimeOffset removeBefore in result.RemoveBefore) foreach (DateTimeOffset removeBefore in result.RemoveBefore)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id) .Where(pi => pi.PlayoutId == playout.Id)
.Where(pi => pi.Finish < removeBefore.UtcDateTime - referenceData.MaxPlayoutOffset) .Where(pi => pi.Finish < removeBefore.UtcDateTime - referenceData.MaxPlayoutOffset)
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
foreach (DateTimeOffset removeAfter in result.RemoveAfter) foreach (DateTimeOffset removeAfter in result.RemoveAfter)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id) .Where(pi => pi.PlayoutId == playout.Id)
.Where(pi => pi.Start >= removeAfter.UtcDateTime) .Where(pi => pi.Start >= removeAfter.UtcDateTime)
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
if (result.ItemsToRemove.Count > 0) if (result.ItemsToRemove.Count > 0)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems
.Where(pi => result.ItemsToRemove.Contains(pi.Id)) .Where(pi => result.ItemsToRemove.Contains(pi.Id))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
if (result.AddedItems.Count > 0) if (result.AddedItems.Count > 0)
{ {
changeCount += 1; changeCount += 1;
bool anyWatermarks = result.AddedItems.Any(i => bool anyWatermarks = result.AddedItems.Any(i =>
i.PlayoutItemWatermarks is not null && i.PlayoutItemWatermarks.Count > 0); i.PlayoutItemWatermarks is not null && i.PlayoutItemWatermarks.Count > 0);
bool anyGraphicsElements = result.AddedItems.Any(i => bool anyGraphicsElements = result.AddedItems.Any(i =>
i.PlayoutItemGraphicsElements is not null && i.PlayoutItemGraphicsElements.Count > 0); i.PlayoutItemGraphicsElements is not null && i.PlayoutItemGraphicsElements.Count > 0);
if (anyWatermarks || anyGraphicsElements) if (anyWatermarks || anyGraphicsElements)
{ {
// need to use slow ef core to also insert watermarks and graphics elements properly // need to use slow ef core to also insert watermarks and graphics elements properly
await dbContext.AddRangeAsync(result.AddedItems, cancellationToken); await dbContext.AddRangeAsync(result.AddedItems, cancellationToken);
} }
else else
{ {
// no watermarks or graphics, bulk insert is ok // no watermarks or graphics, bulk insert is ok
await dbContext.BulkInsertAsync(result.AddedItems, cancellationToken: cancellationToken); await dbContext.BulkInsertAsync(result.AddedItems, cancellationToken: cancellationToken);
} }
} }
if (result.HistoryToRemove.Count > 0) if (result.HistoryToRemove.Count > 0)
{ {
changeCount += await dbContext.PlayoutHistory changeCount += await dbContext.PlayoutHistory
.Where(ph => result.HistoryToRemove.Contains(ph.Id)) .Where(ph => result.HistoryToRemove.Contains(ph.Id))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
if (result.AddedHistory.Count > 0) if (result.AddedHistory.Count > 0)
{ {
changeCount += 1; changeCount += 1;
await dbContext.BulkInsertAsync(result.AddedHistory, cancellationToken: cancellationToken); await dbContext.BulkInsertAsync(result.AddedHistory, cancellationToken: cancellationToken);
} }
// let any active segmenter processes know that the playout has been modified // let any active segmenter processes know that the playout has been modified
// and therefore the segmenter may need to seek into the next item instead of // and therefore the segmenter may need to seek into the next item instead of
// starting at the beginning (if already working ahead) // starting at the beginning (if already working ahead)
changeCount += await dbContext.SaveChangesAsync(cancellationToken); changeCount += await dbContext.SaveChangesAsync(cancellationToken);
bool hasChanges = changeCount > 0; bool hasChanges = changeCount > 0;
if (request.Mode != PlayoutBuildMode.Continue && hasChanges) if (request.Mode != PlayoutBuildMode.Continue && hasChanges)
{ {
_ffmpegSegmenterService.PlayoutUpdated(referenceData.Channel.Number); _ffmpegSegmenterService.PlayoutUpdated(referenceData.Channel.Number);
} }
await _workerChannel.WriteAsync( await _workerChannel.WriteAsync(
new CheckForOverlappingPlayoutItems(request.PlayoutId), new CheckForOverlappingPlayoutItems(request.PlayoutId),
cancellationToken); cancellationToken);
await _workerChannel.WriteAsync(new InsertPlayoutGaps(request.PlayoutId), cancellationToken); await _workerChannel.WriteAsync(new InsertPlayoutGaps(request.PlayoutId), cancellationToken);
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml"); string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName) || if (hasChanges || !File.Exists(fileName) ||
playout.ScheduleKind is PlayoutScheduleKind.ExternalJson) playout.ScheduleKind is PlayoutScheduleKind.ExternalJson)
{ {
await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken); await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken);
// refresh guide data for all mirror channels, too // refresh guide data for all mirror channels, too
List<string> maybeMirrors = await dbContext.Channels List<string> maybeMirrors = await dbContext.Channels
.AsNoTracking() .AsNoTracking()
.Filter(c => c.MirrorSourceChannelId == referenceData.Channel.Id) .Filter(c => c.MirrorSourceChannelId == referenceData.Channel.Id)
.Map(c => c.Number) .Map(c => c.Number)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
foreach (string mirror in maybeMirrors) foreach (string mirror in maybeMirrors)
{ {
await _workerChannel.WriteAsync(new RefreshChannelData(mirror), cancellationToken); await _workerChannel.WriteAsync(new RefreshChannelData(mirror), cancellationToken);
} }
} }
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken);
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken); newBuildStatus.Success = true;
newBuildStatus.Success = true; return result;
},
error =>
{
newBuildStatus.Success = false;
newBuildStatus.Message = error.Value;
return result; return error;
});
} }
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{ {

72
ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
@ -68,7 +69,7 @@ public class PreviewBlockPlayoutHandler(
playout.PlayoutHistory.ToList(), playout.PlayoutHistory.ToList(),
TimeSpan.Zero); TimeSpan.Zero);
PlayoutBuildResult result = Either<BaseError, PlayoutBuildResult> buildResult =
await blockPlayoutBuilder.Build( await blockPlayoutBuilder.Build(
DateTimeOffset.Now, DateTimeOffset.Now,
playout, playout,
@ -76,40 +77,45 @@ public class PreviewBlockPlayoutHandler(
PlayoutBuildMode.Reset, PlayoutBuildMode.Reset,
cancellationToken); cancellationToken);
// load playout item details for title return await buildResult.MatchAsync(
foreach (PlayoutItem playoutItem in result.AddedItems) async result =>
{
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()
.Include(mi => (mi as Movie).MovieMetadata)
.Include(mi => (mi as Movie).MediaVersions)
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
.Include(mi => (mi as MusicVideo).MediaVersions)
.Include(mi => (mi as MusicVideo).Artist)
.ThenInclude(mm => mm.ArtistMetadata)
.Include(mi => (mi as Episode).EpisodeMetadata)
.Include(mi => (mi as Episode).MediaVersions)
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.Include(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.Include(mi => (mi as OtherVideo).MediaVersions)
.Include(mi => (mi as Song).SongMetadata)
.Include(mi => (mi as Song).MediaVersions)
.Include(mi => (mi as Image).ImageMetadata)
.Include(mi => (mi as Image).MediaVersions)
.Include(mi => (mi as RemoteStream).RemoteStreamMetadata)
.Include(mi => (mi as RemoteStream).MediaVersions)
.SelectOneAsync(mi => mi.Id, mi => mi.Id == playoutItem.MediaItemId, cancellationToken);
foreach (MediaItem mediaItem in maybeMediaItem)
{ {
playoutItem.MediaItem = mediaItem; // load playout item details for title
} foreach (PlayoutItem playoutItem in result.AddedItems)
} {
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()
.Include(mi => (mi as Movie).MovieMetadata)
.Include(mi => (mi as Movie).MediaVersions)
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
.Include(mi => (mi as MusicVideo).MediaVersions)
.Include(mi => (mi as MusicVideo).Artist)
.ThenInclude(mm => mm.ArtistMetadata)
.Include(mi => (mi as Episode).EpisodeMetadata)
.Include(mi => (mi as Episode).MediaVersions)
.Include(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.Include(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.Include(mi => (mi as OtherVideo).MediaVersions)
.Include(mi => (mi as Song).SongMetadata)
.Include(mi => (mi as Song).MediaVersions)
.Include(mi => (mi as Image).ImageMetadata)
.Include(mi => (mi as Image).MediaVersions)
.Include(mi => (mi as RemoteStream).RemoteStreamMetadata)
.Include(mi => (mi as RemoteStream).MediaVersions)
.SelectOneAsync(mi => mi.Id, mi => mi.Id == playoutItem.MediaItemId, cancellationToken);
return result.AddedItems.Map(Mapper.ProjectToViewModel).ToList(); foreach (MediaItem mediaItem in maybeMediaItem)
{
playoutItem.MediaItem = mediaItem;
}
}
return result.AddedItems.Map(Mapper.ProjectToViewModel).ToList();
},
_ => []);
} }
private static Block MapToBlock(ReplaceBlockItems request) => private static Block MapToBlock(ReplaceBlockItems request) =>

42
ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs

@ -150,18 +150,22 @@ public class BlockPlayoutBuilderTests
[], [],
TimeSpan.Zero); TimeSpan.Zero);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now, now,
playout, playout,
referenceData, referenceData,
PlayoutBuildMode.Reset, PlayoutBuildMode.Reset,
cancellationToken); cancellationToken);
// this test only cares about "today" buildResult.IsRight.ShouldBeTrue();
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date); foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9)); result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
} }
[Test] [Test]
@ -293,18 +297,22 @@ public class BlockPlayoutBuilderTests
[], [],
TimeSpan.Zero); TimeSpan.Zero);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now, now,
playout, playout,
referenceData, referenceData,
PlayoutBuildMode.Reset, PlayoutBuildMode.Reset,
cancellationToken); cancellationToken);
// this test only cares about "today" buildResult.IsRight.ShouldBeTrue();
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date); foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9)); result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
} }
[Test] [Test]
@ -459,18 +467,22 @@ public class BlockPlayoutBuilderTests
[], [],
TimeSpan.Zero); TimeSpan.Zero);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now, now,
playout, playout,
referenceData, referenceData,
PlayoutBuildMode.Reset, PlayoutBuildMode.Reset,
cancellationToken); cancellationToken);
// this test only cares about "today" buildResult.IsRight.ShouldBeTrue();
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date); foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9)); result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
} }
} }

206
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs

@ -28,7 +28,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -37,8 +37,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(1); buildResult.IsRight.ShouldBeTrue();
result.AddedItems.Head().MediaItemId.ShouldBe(1); foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(1);
}
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);
@ -48,7 +52,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -57,9 +61,13 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish2, finish2,
CancellationToken); CancellationToken);
result2.AddedItems.Count.ShouldBe(1); buildResult2.IsRight.ShouldBeTrue();
result2.AddedItems[0].StartOffset.ShouldBe(finish); foreach (var result2 in buildResult2.RightToSeq())
result2.AddedItems[0].MediaItemId.ShouldBe(2); {
result2.AddedItems.Count.ShouldBe(1);
result2.AddedItems[0].StartOffset.ShouldBe(finish);
result2.AddedItems[0].MediaItemId.ShouldBe(2);
}
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(12)); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(12));
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
@ -80,7 +88,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -89,8 +97,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(1); buildResult.IsRight.ShouldBeTrue();
result.AddedItems.Head().MediaItemId.ShouldBe(1); foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(1);
}
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
@ -99,7 +111,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -108,11 +120,15 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish2, finish2,
CancellationToken); CancellationToken);
result2.AddedItems.Count.ShouldBe(2); buildResult2.IsRight.ShouldBeTrue();
result2.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(6)); foreach (var result2 in buildResult2.RightToSeq())
result2.AddedItems[0].MediaItemId.ShouldBe(2); {
result2.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(12)); result2.AddedItems.Count.ShouldBe(2);
result2.AddedItems[1].MediaItemId.ShouldBe(1); result2.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(6));
result2.AddedItems[0].MediaItemId.ShouldBe(2);
result2.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(12));
result2.AddedItems[1].MediaItemId.ShouldBe(1);
}
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(18)); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(18));
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
@ -133,7 +149,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromDays(1); DateTimeOffset finish = start + TimeSpan.FromDays(1);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -142,8 +158,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(4); buildResult.IsRight.ShouldBeTrue();
result.AddedItems.Map(i => i.MediaItemId).ToList().ShouldBe([1, 2, 1, 2]); foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(4);
result.AddedItems.Map(i => i.MediaItemId).ToList().ShouldBe([1, 2, 1, 2]);
}
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
@ -177,7 +197,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1);
result = await builder.Build( buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -186,9 +206,13 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish2, finish2,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(1); buildResult.IsRight.ShouldBeTrue();
result.AddedItems[0].StartOffset.ShouldBe(finish); foreach (var result in buildResult.RightToSeq())
result.AddedItems[0].MediaItemId.ShouldBe(1); {
result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.ShouldBe(finish);
result.AddedItems[0].MediaItemId.ShouldBe(1);
}
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30)); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30));
@ -199,7 +223,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start3 = HoursAfterMidnight(2); DateTimeOffset start3 = HoursAfterMidnight(2);
DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1); DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1);
result = await builder.Build( buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -208,7 +232,11 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish3, finish3,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(0); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(0);
}
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30)); playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30));
@ -231,7 +259,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -240,7 +268,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(6); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(6);
}
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed.ShouldBeGreaterThan(0); playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed.ShouldBeGreaterThan(0);
@ -251,7 +284,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -281,7 +314,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2); DateTimeOffset finish = start + TimeSpan.FromDays(2);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -290,7 +323,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(53); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(53);
}
playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.Count.ShouldBe(2);
playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue(); playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue();
@ -318,7 +356,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = start.AddHours(1); DateTimeOffset start2 = start.AddHours(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -350,7 +388,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -359,7 +397,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(6); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(6);
}
playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.Count.ShouldBe(2);
PlayoutProgramScheduleAnchor primaryAnchor = PlayoutProgramScheduleAnchor primaryAnchor =
playout.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); playout.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
@ -371,7 +414,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -402,7 +445,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2); DateTimeOffset finish = start + TimeSpan.FromDays(2);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -411,7 +454,12 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(53); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(53);
}
playout.ProgramScheduleAnchors.Count.ShouldBe(4); playout.ProgramScheduleAnchors.Count.ShouldBe(4);
playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue(); playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue();
@ -429,7 +477,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = start.AddHours(i); DateTimeOffset start2 = start.AddHours(i);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -555,7 +603,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(32); DateTimeOffset finish = start + TimeSpan.FromHours(32);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -564,20 +612,24 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(5); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(5);
result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(9)); result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(9));
result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems[0].MediaItemId.ShouldBe(1);
result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(10)); result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(10));
result.AddedItems[1].MediaItemId.ShouldBe(2); result.AddedItems[1].MediaItemId.ShouldBe(2);
result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(11)); result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(11));
result.AddedItems[2].MediaItemId.ShouldBe(1); result.AddedItems[2].MediaItemId.ShouldBe(1);
result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(12)); result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(12));
result.AddedItems[3].MediaItemId.ShouldBe(3); result.AddedItems[3].MediaItemId.ShouldBe(3);
result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(31)); result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(31));
result.AddedItems[4].MediaItemId.ShouldBe(2); result.AddedItems[4].MediaItemId.ShouldBe(2);
}
playout.Anchor.InFlood.ShouldBeTrue(); playout.Anchor.InFlood.ShouldBeTrue();
@ -677,7 +729,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5); DateTimeOffset finish = start + TimeSpan.FromHours(5);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -686,17 +738,21 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(4); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(4);
result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1)); result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1));
result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems[0].MediaItemId.ShouldBe(1);
result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2)); result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2));
result.AddedItems[1].MediaItemId.ShouldBe(1); result.AddedItems[1].MediaItemId.ShouldBe(1);
result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3)); result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3));
result.AddedItems[2].MediaItemId.ShouldBe(2); result.AddedItems[2].MediaItemId.ShouldBe(2);
result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4)); result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4));
result.AddedItems[3].MediaItemId.ShouldBe(2); result.AddedItems[3].MediaItemId.ShouldBe(2);
}
playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(1); playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(1);
playout.Anchor.MultipleRemaining.ShouldBe(1); playout.Anchor.MultipleRemaining.ShouldBe(1);
@ -798,7 +854,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
rerunHelper, rerunHelper,
Logger); Logger);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -807,19 +863,23 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(5); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1)); {
result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems.Count.ShouldBe(5);
result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2));
result.AddedItems[1].MediaItemId.ShouldBe(1); result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1));
result.AddedItems[0].MediaItemId.ShouldBe(1);
result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3)); result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2));
result.AddedItems[2].MediaItemId.ShouldBe(2); result.AddedItems[1].MediaItemId.ShouldBe(1);
result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4));
result.AddedItems[3].MediaItemId.ShouldBe(2); result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3));
result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(5)); result.AddedItems[2].MediaItemId.ShouldBe(2);
result.AddedItems[4].MediaItemId.ShouldBe(2); result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4));
result.AddedItems[3].MediaItemId.ShouldBe(2);
result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(5));
result.AddedItems[4].MediaItemId.ShouldBe(2);
}
playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(0); playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(0);
playout.Anchor.DurationFinish.ShouldBeNull(); playout.Anchor.DurationFinish.ShouldBeNull();

690
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs

File diff suppressed because it is too large Load Diff

33
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs

@ -116,7 +116,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(24); DateTimeOffset start = HoursAfterMidnight(24);
DateTimeOffset finish = start + TimeSpan.FromDays(1); DateTimeOffset finish = start + TimeSpan.FromDays(1);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -125,19 +125,24 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(4); buildResult.IsRight.ShouldBeTrue();
result.AddedItems[0].MediaItemId.ShouldBe(2);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.Zero); foreach (var result in buildResult.RightToSeq())
result.AddedItems[0].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6)); {
result.AddedItems[1].MediaItemId.ShouldBe(3); result.AddedItems.Count.ShouldBe(4);
result.AddedItems[1].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6)); result.AddedItems[0].MediaItemId.ShouldBe(2);
result.AddedItems[1].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(12)); result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.Zero);
result.AddedItems[2].MediaItemId.ShouldBe(1); result.AddedItems[0].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6));
result.AddedItems[2].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(12)); result.AddedItems[1].MediaItemId.ShouldBe(3);
result.AddedItems[2].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(18)); result.AddedItems[1].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6));
result.AddedItems[3].MediaItemId.ShouldBe(2); result.AddedItems[1].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(12));
result.AddedItems[3].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(18)); result.AddedItems[2].MediaItemId.ShouldBe(1);
result.AddedItems[3].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.Zero); result.AddedItems[2].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(12));
result.AddedItems[2].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(18));
result.AddedItems[3].MediaItemId.ShouldBe(2);
result.AddedItems[3].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(18));
result.AddedItems[3].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.Zero);
}
playout.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(48)); playout.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(48));
} }

18
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ResetPlayoutTests.cs

@ -26,7 +26,7 @@ public class ResetPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -35,7 +35,12 @@ public class ResetPlayoutTests : PlayoutBuilderTestBase
finish, finish,
CancellationToken); CancellationToken);
result.AddedItems.Count.ShouldBe(6); buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
result.AddedItems.Count.ShouldBe(6);
}
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
@ -46,7 +51,7 @@ public class ResetPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
PlayoutBuildResult result2 = await builder.Build( Either<BaseError, PlayoutBuildResult> buildResult2 = await builder.Build(
playout, playout,
referenceData, referenceData,
PlayoutBuildResult.Empty, PlayoutBuildResult.Empty,
@ -55,7 +60,12 @@ public class ResetPlayoutTests : PlayoutBuilderTestBase
finish2, finish2,
CancellationToken); CancellationToken);
result2.AddedItems.Count.ShouldBe(6); buildResult2.IsRight.ShouldBeTrue();
foreach (var result in buildResult2.RightToSeq())
{
result.AddedItems.Count.ShouldBe(6);
}
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);

2
ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs

@ -5,7 +5,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IBlockPlayoutBuilder public interface IBlockPlayoutBuilder
{ {
Task<PlayoutBuildResult> Build( Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,

2
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs

@ -8,7 +8,7 @@ public interface IPlayoutBuilder
bool TrimStart { get; set; } bool TrimStart { get; set; }
Playlist DebugPlaylist { get; set; } Playlist DebugPlaylist { get; set; }
Task<PlayoutBuildResult> Build( Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,

2
ErsatzTV.Core/Interfaces/Scheduling/IScriptedPlayoutBuilder.cs

@ -5,7 +5,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IScriptedPlayoutBuilder public interface IScriptedPlayoutBuilder
{ {
Task<PlayoutBuildResult> Build( Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,

2
ErsatzTV.Core/Interfaces/Scheduling/ISequentialPlayoutBuilder.cs

@ -5,7 +5,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface ISequentialPlayoutBuilder public interface ISequentialPlayoutBuilder
{ {
Task<PlayoutBuildResult> Build( Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,

7
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs

@ -27,7 +27,7 @@ public class BlockPlayoutBuilder(
protected virtual ILogger Logger => logger; protected virtual ILogger Logger => logger;
public virtual async Task<PlayoutBuildResult> Build( public virtual async Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
@ -57,6 +57,11 @@ public class BlockPlayoutBuilder(
List<EffectiveBlock> blocksToSchedule = List<EffectiveBlock> blocksToSchedule =
EffectiveBlock.GetEffectiveBlocks(referenceData.PlayoutTemplates, start, daysToBuild); EffectiveBlock.GetEffectiveBlocks(referenceData.PlayoutTemplates, start, daysToBuild);
if (blocksToSchedule.Count == 0)
{
return result;
}
// always start at the beginning of the block // always start at the beginning of the block
start = blocksToSchedule.Min(b => b.Start); start = blocksToSchedule.Min(b => b.Start);

11
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs

@ -26,7 +26,7 @@ public class BlockPlayoutPreviewBuilder(
protected override ILogger Logger => NullLogger.Instance; protected override ILogger Logger => NullLogger.Instance;
public override async Task<PlayoutBuildResult> Build( public override async Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
@ -35,11 +35,16 @@ public class BlockPlayoutPreviewBuilder(
{ {
_randomizedCollections.Add(playout.Channel.UniqueId, []); _randomizedCollections.Add(playout.Channel.UniqueId, []);
PlayoutBuildResult result = await base.Build(start, playout, referenceData, mode, cancellationToken); Either<BaseError, PlayoutBuildResult> buildResult = await base.Build(
start,
playout,
referenceData,
mode,
cancellationToken);
_randomizedCollections.Remove(playout.Channel.UniqueId); _randomizedCollections.Remove(playout.Channel.UniqueId);
return result; return buildResult;
} }
protected override Task<int> GetDaysToBuild(CancellationToken cancellationToken) => Task.FromResult(1); protected override Task<int> GetDaysToBuild(CancellationToken cancellationToken) => Task.FromResult(1);

8
ErsatzTV.Core/Scheduling/PlayoutBuildException.cs

@ -1,8 +0,0 @@
namespace ErsatzTV.Core.Scheduling;
public class PlayoutBuildException : Exception
{
public PlayoutBuildException() : base() { }
public PlayoutBuildException(string message) : base(message) { }
public PlayoutBuildException(string message, Exception innerException) : base(message, innerException) { }
}

132
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -63,15 +63,13 @@ public class PlayoutBuilder : IPlayoutBuilder
} }
} }
public async Task<PlayoutBuildResult> Build( public async Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildMode mode, PlayoutBuildMode mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
PlayoutBuildResult result = PlayoutBuildResult.Empty;
if (playout.ScheduleKind is not PlayoutScheduleKind.Classic) if (playout.ScheduleKind is not PlayoutScheduleKind.Classic)
{ {
_logger.LogWarning( _logger.LogWarning(
@ -80,30 +78,39 @@ public class PlayoutBuilder : IPlayoutBuilder
referenceData.Channel.Number, referenceData.Channel.Number,
referenceData.Channel.Name); referenceData.Channel.Name);
return result; return BaseError.New($"Cannot build playout type {playout.ScheduleKind} using classic playout builder.");
} }
foreach (PlayoutParameters parameters in await Validate(start, playout, referenceData, cancellationToken)) Either<BaseError, PlayoutParameters> validateResult = await Validate(start, referenceData, cancellationToken);
{ return await validateResult.MatchAsync(
// for testing purposes async parameters =>
// if (mode == PlayoutBuildMode.Reset)
// {
// return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) });
// }
result = await Build(playout, referenceData, result, mode, parameters, cancellationToken);
result = result with
{ {
RerunHistoryToRemove = _rerunHelper.GetHistoryToRemove(), // for testing purposes
AddedRerunHistory = _rerunHelper.GetHistoryToAdd() // if (mode == PlayoutBuildMode.Reset)
}; // {
} // return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) });
// }
return result; Either<BaseError, PlayoutBuildResult> buildResult = await Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
mode,
parameters,
cancellationToken);
return buildResult.Match(
result => result with
{
RerunHistoryToRemove = _rerunHelper.GetHistoryToRemove(),
AddedRerunHistory = _rerunHelper.GetHistoryToAdd()
},
Either<BaseError, PlayoutBuildResult>.Left);
},
Either<BaseError, PlayoutBuildResult>.Left);
} }
private Task<PlayoutBuildResult> Build( private Task<Either<BaseError, PlayoutBuildResult>> Build(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,
@ -117,7 +124,7 @@ public class PlayoutBuilder : IPlayoutBuilder
_ => ContinuePlayout(playout, referenceData, result, parameters, cancellationToken) _ => ContinuePlayout(playout, referenceData, result, parameters, cancellationToken)
}; };
internal async Task<PlayoutBuildResult> Build( internal async Task<Either<BaseError, PlayoutBuildResult>> Build(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,
@ -126,21 +133,19 @@ public class PlayoutBuilder : IPlayoutBuilder
DateTimeOffset finish, DateTimeOffset finish,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
foreach (PlayoutParameters parameters in await Validate(start, playout, referenceData, cancellationToken)) Either<BaseError, PlayoutParameters> validateResult = await Validate(start, referenceData, cancellationToken);
{ return await validateResult.MatchAsync(
result = await Build( async parameters => await Build(
playout, playout,
referenceData, referenceData,
result, result,
mode, mode,
parameters with { Start = start, Finish = finish }, parameters with { Start = start, Finish = finish },
cancellationToken); cancellationToken),
} Either<BaseError, PlayoutBuildResult>.Left);
return result;
} }
private async Task<PlayoutBuildResult> RefreshPlayout( private async Task<Either<BaseError, PlayoutBuildResult>> RefreshPlayout(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,
@ -266,7 +271,7 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken); cancellationToken);
} }
private async Task<PlayoutBuildResult> ResetPlayout( private async Task<Either<BaseError, PlayoutBuildResult>> ResetPlayout(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,
@ -310,7 +315,7 @@ public class PlayoutBuilder : IPlayoutBuilder
return result; return result;
} }
private async Task<PlayoutBuildResult> ContinuePlayout( private async Task<Either<BaseError, PlayoutBuildResult>> ContinuePlayout(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,
@ -340,9 +345,8 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken); cancellationToken);
} }
private async Task<Option<PlayoutParameters>> Validate( private async Task<Either<BaseError, PlayoutParameters>> Validate(
DateTimeOffset start, DateTimeOffset start,
Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -351,7 +355,7 @@ public class PlayoutBuilder : IPlayoutBuilder
if (collectionMediaItems.IsEmpty) if (collectionMediaItems.IsEmpty)
{ {
_logger.LogWarning("Playout {Playout} has no items", referenceData.Channel.Name); _logger.LogWarning("Playout {Playout} has no items", referenceData.Channel.Name);
return None; return BaseError.New($"Playout {referenceData.Channel.Name} has no items");
} }
Option<bool> skipMissingItems = Option<bool> skipMissingItems =
@ -365,24 +369,26 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
Option<string> maybeName = Option<string> maybeName =
await _mediaCollectionRepository.GetNameFromKey(emptyCollection, cancellationToken); await _mediaCollectionRepository.GetNameFromKey(emptyCollection, cancellationToken);
if (maybeName.IsSome) return maybeName.Match(
{ name =>
foreach (string name in maybeName)
{ {
_logger.LogError( _logger.LogError(
"Unable to rebuild playout; {CollectionType} {CollectionName} has no valid items!", "Unable to rebuild playout; {CollectionType} {CollectionName} has no valid items!",
emptyCollection.CollectionType, emptyCollection.CollectionType,
name); name);
}
}
else
{
_logger.LogError(
"Unable to rebuild playout; collection {@CollectionKey} has no valid items!",
emptyCollection);
}
return None; return BaseError.New(
$"Unable to rebuild playout; {emptyCollection.CollectionType} {name} has no valid items!");
},
() =>
{
_logger.LogError(
"Unable to rebuild playout; collection {@CollectionKey} has no valid items!",
emptyCollection);
return BaseError.New(
$"Unable to rebuild playout; collection {HistoryDetails.KeyForCollectionKey(emptyCollection)} has no valid items!");
});
} }
Option<int> daysToBuild = await _configElementRepository.GetValue<int>( Option<int> daysToBuild = await _configElementRepository.GetValue<int>(
@ -397,7 +403,7 @@ public class PlayoutBuilder : IPlayoutBuilder
collectionMediaItems); collectionMediaItems);
} }
private async Task<PlayoutBuildResult> BuildPlayoutItems( private async Task<Either<BaseError, PlayoutBuildResult>> BuildPlayoutItems(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,
@ -431,11 +437,11 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return result; return BaseError.New("Playout build was canceled");
} }
_logger.LogDebug("Building playout from {Start} to {Finish}", start, finish); _logger.LogDebug("Building playout from {Start} to {Finish}", start, finish);
result = await BuildPlayoutItems( Either<BaseError, PlayoutBuildResult> buildResult = await BuildPlayoutItems(
playout, playout,
referenceData, referenceData,
result, result,
@ -446,6 +452,16 @@ public class PlayoutBuilder : IPlayoutBuilder
randomStartPoint, randomStartPoint,
cancellationToken); cancellationToken);
foreach (BaseError error in buildResult.LeftToSeq())
{
return error;
}
foreach (PlayoutBuildResult r in buildResult.RightToSeq())
{
result = r;
}
// only randomize once (at the start of the playout) // only randomize once (at the start of the playout)
randomStartPoint = false; randomStartPoint = false;
@ -455,14 +471,14 @@ public class PlayoutBuilder : IPlayoutBuilder
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return result; return BaseError.New("Playout build was canceled");
} }
if (start < playoutFinish) if (start < playoutFinish)
{ {
// build one final time without continue anchors // build one final time without continue anchors
_logger.LogDebug("Building final playout from {Start} to {Finish}", start, playoutFinish); _logger.LogDebug("Building final playout from {Start} to {Finish}", start, playoutFinish);
result = await BuildPlayoutItems( Either<BaseError, PlayoutBuildResult> buildResult = await BuildPlayoutItems(
playout, playout,
referenceData, referenceData,
result, result,
@ -472,6 +488,16 @@ public class PlayoutBuilder : IPlayoutBuilder
false, false,
randomStartPoint, randomStartPoint,
cancellationToken); cancellationToken);
foreach (BaseError error in buildResult.LeftToSeq())
{
return error;
}
foreach (PlayoutBuildResult r in buildResult.RightToSeq())
{
result = r;
}
} }
if (TrimStart) if (TrimStart)
@ -504,7 +530,7 @@ public class PlayoutBuilder : IPlayoutBuilder
return result; return result;
} }
private async Task<PlayoutBuildResult> BuildPlayoutItems( private async Task<Either<BaseError, PlayoutBuildResult>> BuildPlayoutItems(
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
PlayoutBuildResult result, PlayoutBuildResult result,

10
ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs

@ -18,7 +18,7 @@ public class ScriptedPlayoutBuilder(
ILogger<ScriptedPlayoutBuilder> logger) ILogger<ScriptedPlayoutBuilder> logger)
: IScriptedPlayoutBuilder : IScriptedPlayoutBuilder
{ {
public async Task<PlayoutBuildResult> Build( public async Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
@ -43,7 +43,7 @@ public class ScriptedPlayoutBuilder(
logger.LogError( logger.LogError(
"Cannot build scripted playout; schedule file {File} does not exist", "Cannot build scripted playout; schedule file {File} does not exist",
scriptFile); scriptFile);
return result; return BaseError.New($"Cannot build scripted playout; schedule file {scriptFile} does not exist");
} }
var arguments = new List<string> var arguments = new List<string>
@ -102,6 +102,8 @@ public class ScriptedPlayoutBuilder(
"Scripted playout process exited with code {Code}: {Error}", "Scripted playout process exited with code {Code}: {Error}",
commandResult.ExitCode, commandResult.ExitCode,
commandResult.StandardError); commandResult.StandardError);
return BaseError.New(
$"Scripted playout process exited with code {commandResult.ExitCode}: {commandResult.StandardError}");
} }
playout.Anchor = schedulingEngine.GetAnchor(); playout.Anchor = schedulingEngine.GetAnchor();
@ -111,12 +113,12 @@ public class ScriptedPlayoutBuilder(
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
logger.LogWarning("Scripted playout build timed out after {TimeoutSeconds} seconds", timeoutSeconds); logger.LogWarning("Scripted playout build timed out after {TimeoutSeconds} seconds", timeoutSeconds);
throw new TimeoutException($"Scripted playout build timed out after {timeoutSeconds} seconds"); return BaseError.New($"Scripted playout build timed out after {timeoutSeconds} seconds");
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogWarning(ex, "Unexpected exception building scripted playout"); logger.LogWarning(ex, "Unexpected exception building scripted playout");
throw; return BaseError.New($"Unexpected exception building scripted playout: {ex}");
} }
finally finally
{ {

14
ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs

@ -26,7 +26,7 @@ public class SequentialPlayoutBuilder(
ILogger<SequentialPlayoutBuilder> logger) ILogger<SequentialPlayoutBuilder> logger)
: ISequentialPlayoutBuilder : ISequentialPlayoutBuilder
{ {
public async Task<PlayoutBuildResult> Build( public async Task<Either<BaseError, PlayoutBuildResult>> Build(
DateTimeOffset start, DateTimeOffset start,
Playout playout, Playout playout,
PlayoutReferenceData referenceData, PlayoutReferenceData referenceData,
@ -41,7 +41,7 @@ public class SequentialPlayoutBuilder(
if (!localFileSystem.FileExists(playout.ScheduleFile)) if (!localFileSystem.FileExists(playout.ScheduleFile))
{ {
logger.LogWarning("Sequential schedule file {File} does not exist; aborting.", playout.ScheduleFile); logger.LogWarning("Sequential schedule file {File} does not exist; aborting.", playout.ScheduleFile);
throw new PlayoutBuildException($"Sequential schedule file {playout.ScheduleFile} does not exist"); return BaseError.New($"Sequential schedule file {playout.ScheduleFile} does not exist");
} }
Option<YamlPlayoutDefinition> maybePlayoutDefinition = Option<YamlPlayoutDefinition> maybePlayoutDefinition =
@ -49,7 +49,7 @@ public class SequentialPlayoutBuilder(
if (maybePlayoutDefinition.IsNone) if (maybePlayoutDefinition.IsNone)
{ {
logger.LogWarning("Sequential schedule file {File} is invalid; aborting.", playout.ScheduleFile); logger.LogWarning("Sequential schedule file {File} is invalid; aborting.", playout.ScheduleFile);
throw new PlayoutBuildException($"Sequential schedule file {playout.ScheduleFile} is invalid"); return BaseError.New($"Sequential schedule file {playout.ScheduleFile} is invalid");
} }
// using ValueUnsafe to avoid nesting // using ValueUnsafe to avoid nesting
@ -96,13 +96,13 @@ public class SequentialPlayoutBuilder(
if (maybeImportedDefinition.IsNone) if (maybeImportedDefinition.IsNone)
{ {
logger.LogWarning("YAML playout import {File} is invalid; aborting.", import); logger.LogWarning("YAML playout import {File} is invalid; aborting.", import);
throw new PlayoutBuildException($"YAML playout import {import} is invalid"); return BaseError.New($"YAML playout import {import} is invalid");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Unexpected exception loading YAML playout import"); logger.LogError(ex, "Unexpected exception loading YAML playout import");
throw new PlayoutBuildException("Unexpected exception loading YAML playout import", ex); return BaseError.New($"Unexpected exception loading YAML playout import: {ex}");
} }
} }
@ -236,7 +236,7 @@ public class SequentialPlayoutBuilder(
if (DetectCycle(context.Definition)) if (DetectCycle(context.Definition))
{ {
logger.LogError("YAML sequence contains a cycle; unable to build playout"); logger.LogError("YAML sequence contains a cycle; unable to build playout");
throw new PlayoutBuildException("YAML sequence contains a cycle; unable to build playout"); return BaseError.New("YAML sequence contains a cycle; unable to build playout");
} }
var flattenCount = 0; var flattenCount = 0;
@ -536,7 +536,7 @@ public class SequentialPlayoutBuilder(
catch (Exception ex) catch (Exception ex)
{ {
logger.LogWarning(ex, "Error loading YAML playout definition"); logger.LogWarning(ex, "Error loading YAML playout definition");
throw; return Option<YamlPlayoutDefinition>.None;
} }
} }

Loading…
Cancel
Save