Browse Source

refactor playout building

pull/2273/head
Jason Dove 3 days ago
parent
commit
855c599009
No known key found for this signature in database
  1. 4
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs
  3. 23
      ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs
  4. 271
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  5. 2
      ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs
  6. 2
      ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs
  7. 18
      ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs
  8. 1252
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  9. 1
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  10. 132
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  11. 1
      ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs
  12. 2
      ErsatzTV.Core/Domain/ProgramSchedulePlayoutType.cs
  13. 6
      ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs
  14. 7
      ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutFillerBuilder.cs
  15. 6
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs
  16. 6
      ErsatzTV.Core/Interfaces/Scheduling/IYamlPlayoutBuilder.cs
  17. 62
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  18. 20
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutChangeDetection.cs
  19. 29
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs
  20. 34
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs
  21. 11
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs
  22. 17
      ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs
  23. 181
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  24. 1
      ErsatzTV.Core/Scheduling/PlayoutBuilderState.cs
  25. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  26. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  27. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  28. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  29. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  30. 13
      ErsatzTV.Core/Scheduling/PlayoutReferenceData.cs
  31. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs
  32. 5
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs
  33. 6
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  34. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  35. 13
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  36. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs
  37. 49
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  38. 5
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs
  39. 1
      ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
  40. 1
      ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj
  41. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  42. 1
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  43. 4
      ErsatzTV/Pages/Playouts.razor

4
CHANGELOG.md

@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `channel_seconds` - the total number of seconds the frame is from when the channel started/activated
- `time_of_day_seconds` - the total number of seconds the frame is since midnight
### Fix
- Fix database operations that were slowing down playout builds
- YAML playouts in particular should build significantly faster
### Changed
- Allow multiple watermarks on a single playout item
- YAML playout: `watermark` instruction changes:

2
ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs

@ -192,7 +192,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -192,7 +192,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
{
switch (playout.ProgramSchedulePlayoutType)
{
case ProgramSchedulePlayoutType.Flood:
case ProgramSchedulePlayoutType.Classic:
case ProgramSchedulePlayoutType.Yaml:
var floodSorted = playouts
.Collect(p => p.Items)

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
@ -31,20 +32,30 @@ public class PreviewPlaylistPlayoutHandler( @@ -31,20 +32,30 @@ public class PreviewPlaylistPlayoutHandler(
Name = "Playlist Preview"
},
Items = [],
ProgramSchedulePlayoutType = ProgramSchedulePlayoutType.Flood,
ProgramSchedulePlayoutType = ProgramSchedulePlayoutType.Classic,
PlayoutHistory = [],
ProgramSchedule = new ProgramSchedule
{
Items = [MapToScheduleItem(request)]
},
ProgramScheduleAlternates = [],
FillGroupIndices = []
FillGroupIndices = [],
Templates = []
};
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
playout.Items,
playout.Templates.ToList(),
new ProgramSchedule(),
playout.ProgramScheduleAlternates.ToList(),
playout.PlayoutHistory.ToList());
// TODO: make an explicit method to preview, this is ugly
playoutBuilder.TrimStart = false;
playoutBuilder.DebugPlaylist = playout.ProgramSchedule.Items[0].Playlist;
await playoutBuilder.Build(playout, PlayoutBuildMode.Reset, cancellationToken);
var result = await playoutBuilder.Build(playout, referenceData, PlayoutBuildMode.Reset, cancellationToken);
var maxItems = 0;
Dictionary<PlaylistItem, List<MediaItem>> map =
@ -62,10 +73,10 @@ public class PreviewPlaylistPlayoutHandler( @@ -62,10 +73,10 @@ public class PreviewPlaylistPlayoutHandler(
}
// limit preview to once through the playlist
playout.Items = playout.Items.Take(maxItems).ToList();
var onceThrough = result.AddedItems.Take(maxItems).ToList();
// load playout item details for title
foreach (PlayoutItem playoutItem in playout.Items)
foreach (PlayoutItem playoutItem in onceThrough)
{
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()
@ -95,7 +106,7 @@ public class PreviewPlaylistPlayoutHandler( @@ -95,7 +106,7 @@ public class PreviewPlaylistPlayoutHandler(
}
}
return playout.Items.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) =>

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

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
using System.Threading.Channels;
using Bugsnag;
using Dapper;
using EFCore.BulkExtensions;
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Scheduling;
@ -67,54 +68,97 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -67,54 +68,97 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
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;
channelName = referenceData.Channel.Name;
var result = PlayoutBuildResult.Empty;
switch (playout.ProgramSchedulePlayoutType)
{
case ProgramSchedulePlayoutType.Block:
await _blockPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
await _blockPlayoutFillerBuilder.Build(playout, request.Mode, cancellationToken);
result = await _blockPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
result = await _blockPlayoutFillerBuilder.Build(playout, referenceData, result, request.Mode, cancellationToken);
break;
case ProgramSchedulePlayoutType.Yaml:
await _yamlPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
result = await _yamlPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
break;
case ProgramSchedulePlayoutType.ExternalJson:
await _externalJsonPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
break;
case ProgramSchedulePlayoutType.None:
case ProgramSchedulePlayoutType.Flood:
case ProgramSchedulePlayoutType.Classic:
default:
await _playoutBuilder.Build(playout, request.Mode, cancellationToken);
result = await _playoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
break;
}
int changeCount = 0;
if (result.ClearItems)
{
changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id)
.ExecuteDeleteAsync(cancellationToken);
}
foreach (var removeBefore in result.RemoveBefore)
{
changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id)
.Where(pi => pi.Finish < removeBefore.UtcDateTime)
.ExecuteDeleteAsync(cancellationToken);
}
foreach (var removeAfter in result.RemoveAfter)
{
changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id)
.Where(pi => pi.Start >= removeAfter.UtcDateTime)
.ExecuteDeleteAsync(cancellationToken);
}
if (result.AddedItems.Count > 0)
{
changeCount += 1;
await dbContext.BulkInsertAsync(result.AddedItems, cancellationToken: cancellationToken);
}
if (result.HistoryToRemove.Count > 0)
{
changeCount += await dbContext.PlayoutHistory
.Where(ph => result.HistoryToRemove.Contains(ph.Id))
.ExecuteDeleteAsync(cancellationToken);
}
if (result.AddedHistory.Count > 0)
{
changeCount += 1;
await dbContext.BulkInsertAsync(result.AddedHistory, cancellationToken: cancellationToken);
}
// 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
// starting at the beginning (if already working ahead)
bool hasChanges = await dbContext.SaveChangesAsync(cancellationToken) > 0;
changeCount += await dbContext.SaveChangesAsync(cancellationToken);
bool hasChanges = changeCount > 0;
if (request.Mode != PlayoutBuildMode.Continue && hasChanges)
{
_ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number);
_ffmpegSegmenterService.PlayoutUpdated(referenceData.Channel.Number);
}
Option<string> maybeChannelNumber = await dbContext.Connection
.QuerySingleOrDefaultAsync<string>(
@"select C.Number from Channel C
inner join Playout P on C.Id = P.ChannelId
where P.Id = @PlayoutId",
new { request.PlayoutId })
.Map(Optional);
foreach (string channelNumber in maybeChannelNumber)
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName) ||
playout.ProgramSchedulePlayoutType is ProgramSchedulePlayoutType.ExternalJson)
{
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName) ||
playout.ProgramSchedulePlayoutType is ProgramSchedulePlayoutType.ExternalJson)
{
await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken);
}
await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken);
}
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken);
@ -123,7 +167,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -123,7 +167,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
_client.Notify(ex);
return BaseError.New(
$"Timeout building playout for channel {playout.Channel.Name}; this may be a bug!");
$"Timeout building playout for channel {channelName}; this may be a bug!");
}
catch (Exception ex)
{
@ -131,7 +175,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -131,7 +175,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_client.Notify(ex);
return BaseError.New(
$"Unexpected error building playout for channel {playout.Channel.Name}: {ex.Message}");
$"Unexpected error building playout for channel {channelName}: {ex.Message}");
}
finally
{
@ -159,86 +203,149 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -159,86 +203,149 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
return playout;
}
private static Task<Validation<BaseError, Playout>> PlayoutMustExist(
private static async Task<Validation<BaseError, Playout>> PlayoutMustExist(
TvContext dbContext,
BuildPlayout buildPlayout) =>
dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Deco)
.Include(p => p.Items)
.ThenInclude(pi => pi.Watermarks)
.Include(p => p.PlayoutHistory)
.Include(p => p.Templates)
.ThenInclude(t => t.Template)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Block)
.ThenInclude(b => b.Items)
.Include(p => p.Templates)
.ThenInclude(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.Include(p => p.FillGroupIndices)
.ThenInclude(fgi => fgi.EnumeratorState)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
BuildPlayout buildPlayout)
{
var maybePlayout = await dbContext.Playouts
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId);
foreach (var playout in maybePlayout)
{
switch (playout.ProgramSchedulePlayoutType)
{
case ProgramSchedulePlayoutType.Classic:
await dbContext.Entry(playout)
.Collection(p => p.FillGroupIndices)
.LoadAsync();
foreach (var fillGroupIndex in playout.FillGroupIndices)
{
await dbContext.Entry(fillGroupIndex)
.Reference(fgi => fgi.EnumeratorState)
.LoadAsync();
}
await dbContext.Entry(playout)
.Collection(p => p.ProgramScheduleAnchors)
.LoadAsync();
foreach (var anchor in playout.ProgramScheduleAnchors)
{
await dbContext.Entry(anchor)
.Reference(a => a.EnumeratorState)
.LoadAsync();
}
break;
}
}
return maybePlayout.ToValidation<BaseError>("Playout does not exist.");
}
private static async Task<PlayoutReferenceData> GetReferenceData(
TvContext dbContext,
int playoutId,
ProgramSchedulePlayoutType playoutType)
{
var channel = await dbContext.Channels
.AsNoTracking()
.Where(c => c.Playouts.Any(p => p.Id == playoutId))
.FirstOrDefaultAsync();
var deco = Option<Deco>.None;
List<PlayoutItem> existingItems = [];
List<PlayoutTemplate> playoutTemplates = [];
if (playoutType is ProgramSchedulePlayoutType.Block)
{
deco = await dbContext.Decos
.AsNoTracking()
.Where(d => d.Playouts.Any(p => p.Id == playoutId))
.FirstOrDefaultAsync()
.Map(Optional);
existingItems = await dbContext.PlayoutItems
.AsNoTracking()
.Where(pi => pi.PlayoutId == playoutId)
.ToListAsync();
playoutTemplates = await dbContext.PlayoutTemplates
.AsNoTracking()
.Where(pt => pt.PlayoutId == playoutId)
.Include(t => t.Template)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Block)
.ThenInclude(b => b.Items)
.Include(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.ToListAsync();
}
var programSchedule = await dbContext.ProgramSchedules
.AsNoTracking()
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
.Include(ps => ps.Items)
.ThenInclude(psi => psi.Watermark)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(psa => psa.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.FirstOrDefaultAsync();
var programScheduleAlternates = await dbContext.ProgramScheduleAlternates
.AsNoTracking()
.Where(pt => pt.PlayoutId == playoutId)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Watermark)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
.ToListAsync();
var playoutHistory = await dbContext.PlayoutHistory
.AsNoTracking()
.Where(h => h.PlayoutId == playoutId)
.ToListAsync();
return new PlayoutReferenceData(
channel,
deco,
existingItems,
playoutTemplates,
programSchedule,
programScheduleAlternates,
playoutHistory);
}
}

2
ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs

@ -7,7 +7,7 @@ public record CreatePlayout(int ChannelId, ProgramSchedulePlayoutType ProgramSch @@ -7,7 +7,7 @@ public record CreatePlayout(int ChannelId, ProgramSchedulePlayoutType ProgramSch
: IRequest<Either<BaseError, CreatePlayoutResponse>>;
public record CreateFloodPlayout(int ChannelId, int ProgramScheduleId)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Flood);
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Classic);
public record CreateBlockPlayout(int ChannelId)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Block);

2
ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs

@ -21,7 +21,7 @@ public class ResetAllPlayoutsHandler( @@ -21,7 +21,7 @@ public class ResetAllPlayoutsHandler(
{
switch (playout.ProgramSchedulePlayoutType)
{
case ProgramSchedulePlayoutType.Flood:
case ProgramSchedulePlayoutType.Classic:
case ProgramSchedulePlayoutType.Block:
case ProgramSchedulePlayoutType.Yaml:
if (!locker.IsPlayoutLocked(playout.Id))

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

@ -53,13 +53,25 @@ public class PreviewBlockPlayoutHandler( @@ -53,13 +53,25 @@ public class PreviewBlockPlayoutHandler(
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
Template = template
}
]
],
ProgramSchedule = new ProgramSchedule(),
ProgramScheduleAlternates = []
};
await blockPlayoutBuilder.Build(playout, PlayoutBuildMode.Reset, cancellationToken);
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
playout.Items,
playout.Templates.ToList(),
playout.ProgramSchedule,
playout.ProgramScheduleAlternates,
playout.PlayoutHistory.ToList());
PlayoutBuildResult result =
await blockPlayoutBuilder.Build(playout, referenceData, PlayoutBuildMode.Reset, cancellationToken);
// load playout item details for title
foreach (PlayoutItem playoutItem in playout.Items)
foreach (PlayoutItem playoutItem in result.AddedItems)
{
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()

1252
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

File diff suppressed because it is too large Load Diff

1
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs

@ -741,6 +741,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase @@ -741,6 +741,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
IScheduleItemsEnumerator enumerator = Substitute.For<IScheduleItemsEnumerator>();
var state = new PlayoutBuilderState(
0,
enumerator,
None,
None,

132
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -2,6 +2,7 @@ using Bugsnag; @@ -2,6 +2,7 @@ using Bugsnag;
using Dapper;
using Destructurama;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
@ -130,9 +131,11 @@ public class ScheduleIntegrationTests @@ -130,9 +131,11 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, PLAYOUT_ID, ProgramSchedulePlayoutType.Classic);
await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
// TODO: would need to apply changes from build result
await context.SaveChangesAsync(_cancellationToken);
}
@ -142,14 +145,18 @@ public class ScheduleIntegrationTests @@ -142,14 +145,18 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, PLAYOUT_ID, ProgramSchedulePlayoutType.Classic);
await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start.AddHours(i),
finish.AddHours(i),
_cancellationToken);
// TODO: would need to apply changes from build result
await context.SaveChangesAsync(_cancellationToken);
}
@ -159,14 +166,18 @@ public class ScheduleIntegrationTests @@ -159,14 +166,18 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, PLAYOUT_ID, ProgramSchedulePlayoutType.Classic);
await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start.AddHours(i),
finish.AddHours(i),
_cancellationToken);
// TODO: would need to apply changes from build result
await context.SaveChangesAsync(_cancellationToken);
}
}
@ -301,14 +312,18 @@ public class ScheduleIntegrationTests @@ -301,14 +312,18 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, playoutId);
Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, playoutId, ProgramSchedulePlayoutType.Classic);
await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start.AddHours(i),
finish.AddHours(i),
_cancellationToken);
// TODO: would need to apply changes from build result
await context.SaveChangesAsync(_cancellationToken);
}
}
@ -358,60 +373,105 @@ public class ScheduleIntegrationTests @@ -358,60 +373,105 @@ public class ScheduleIntegrationTests
private static async Task<Option<Playout>> GetPlayout(TvContext dbContext, int playoutId) =>
await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.EnumeratorState)
.SelectOneAsync(p => p.Id, p => p.Id == playoutId);
private static async Task<PlayoutReferenceData> GetReferenceData(
TvContext dbContext,
int playoutId,
ProgramSchedulePlayoutType playoutType)
{
var channel = await dbContext.Channels
.AsNoTracking()
.Where(c => c.Playouts.Any(p => p.Id == playoutId))
.FirstOrDefaultAsync();
List<PlayoutItem> existingItems = [];
List<PlayoutTemplate> playoutTemplates = [];
if (playoutType is ProgramSchedulePlayoutType.Block)
{
existingItems = await dbContext.PlayoutItems
.AsNoTracking()
.Where(pi => pi.PlayoutId == playoutId)
.ToListAsync();
playoutTemplates = await dbContext.PlayoutTemplates
.AsNoTracking()
.Where(pt => pt.PlayoutId == playoutId)
.Include(t => t.Template)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Block)
.ThenInclude(b => b.Items)
.Include(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.ToListAsync();
}
var programSchedule = await dbContext.ProgramSchedules
.AsNoTracking()
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
.Include(ps => ps.Items)
.ThenInclude(psi => psi.Watermark)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.Include(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.FirstOrDefaultAsync();
var programScheduleAlternates = await dbContext.ProgramScheduleAlternates
.AsNoTracking()
.Where(pt => pt.PlayoutId == playoutId)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Watermark)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramSchedule)
.Include(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == playoutId);
.ToListAsync();
var playoutHistory = await dbContext.PlayoutHistory
.AsNoTracking()
.Where(h => h.PlayoutId == playoutId)
.ToListAsync();
return new PlayoutReferenceData(
channel,
Option<Deco>.None,
existingItems,
playoutTemplates,
programSchedule,
programScheduleAlternates,
playoutHistory);
}
}

1
ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs

@ -13,6 +13,7 @@ public abstract class SchedulerTestBase @@ -13,6 +13,7 @@ public abstract class SchedulerTestBase
};
protected static PlayoutBuilderState StartState(IScheduleItemsEnumerator scheduleItemsEnumerator) => new(
1,
scheduleItemsEnumerator,
None,
None,

2
ErsatzTV.Core/Domain/ProgramSchedulePlayoutType.cs

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
public enum ProgramSchedulePlayoutType
{
None = 0,
Flood = 1,
Classic = 1,
Block = 2,
Yaml = 3,

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

@ -5,5 +5,9 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; @@ -5,5 +5,9 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IBlockPlayoutBuilder
{
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken);
}

7
ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutFillerBuilder.cs

@ -5,5 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; @@ -5,5 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IBlockPlayoutFillerBuilder
{
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutBuildMode mode,
CancellationToken cancellationToken);
}

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

@ -8,5 +8,9 @@ public interface IPlayoutBuilder @@ -8,5 +8,9 @@ public interface IPlayoutBuilder
public bool TrimStart { get; set; }
public Playlist DebugPlaylist { get; set; }
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken);
}

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

@ -5,5 +5,9 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; @@ -5,5 +5,9 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IYamlPlayoutBuilder
{
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken);
}

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

@ -27,16 +27,19 @@ public class BlockPlayoutBuilder( @@ -27,16 +27,19 @@ public class BlockPlayoutBuilder(
protected virtual ILogger Logger => logger;
public virtual async Task<Playout> Build(
public virtual async Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken)
{
var result = PlayoutBuildResult.Empty;
Logger.LogDebug(
"Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
referenceData.Channel.Number,
referenceData.Channel.Name);
List<PlaybackOrder> allowedPlaybackOrders =
[
@ -53,38 +56,41 @@ public class BlockPlayoutBuilder( @@ -53,38 +56,41 @@ public class BlockPlayoutBuilder(
// get blocks to schedule
List<EffectiveBlock> blocksToSchedule =
EffectiveBlock.GetEffectiveBlocks(playout.Templates, start, daysToBuild);
EffectiveBlock.GetEffectiveBlocks(referenceData.PlayoutTemplates, start, daysToBuild);
// get all collection items for the playout
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule);
if (collectionMediaItems.Values.All(v => v.Count == 0))
{
logger.LogWarning("There are no media items to schedule");
return playout;
return result;
}
Map<CollectionKey, string> collectionEtags = GetCollectionEtags(collectionMediaItems);
Dictionary<PlayoutItem, BlockKey> itemBlockKeys =
BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(playout);
BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(referenceData);
// remove items without a block key (shouldn't happen often, just upgrades)
playout.Items.RemoveAll(i => !itemBlockKeys.ContainsKey(i));
foreach (var item in referenceData.ExistingItems.Where(i => !itemBlockKeys.ContainsKey(i)))
{
result.ItemsToRemove.Add(item.Id);
}
// remove old items
// importantly, this should not remove their history
playout.Items.RemoveAll(i => i.FinishOffset < start);
result = result with { RemoveBefore = start };
(List<EffectiveBlock> updatedEffectiveBlocks, List<PlayoutItem> playoutItemsToRemove) =
BlockPlayoutChangeDetection.FindUpdatedItems(
playout.Items,
referenceData.ExistingItems,
itemBlockKeys,
blocksToSchedule,
collectionEtags);
foreach (PlayoutItem playoutItem in playoutItemsToRemove)
{
BlockPlayoutChangeDetection.RemoveItemAndHistory(playout, playoutItem);
result = BlockPlayoutChangeDetection.RemoveItemAndHistory(playout, playoutItem, result);
}
DateTimeOffset currentTime = start;
@ -140,6 +146,7 @@ public class BlockPlayoutBuilder( @@ -140,6 +146,7 @@ public class BlockPlayoutBuilder(
IMediaCollectionEnumerator enumerator = GetEnumerator(
playout,
referenceData,
blockItem,
currentTime,
historyKey,
@ -161,6 +168,7 @@ public class BlockPlayoutBuilder( @@ -161,6 +168,7 @@ public class BlockPlayoutBuilder(
// create a playout item
var playoutItem = new PlayoutItem
{
PlayoutId = playout.Id,
MediaItemId = mediaItem.Id,
Start = currentTime.UtcDateTime,
Finish = currentTime.UtcDateTime + itemDuration,
@ -195,7 +203,7 @@ public class BlockPlayoutBuilder( @@ -195,7 +203,7 @@ public class BlockPlayoutBuilder(
break;
}
playout.Items.Add(playoutItem);
result.AddedItems.Add(playoutItem);
// create a playout history record
var nextHistory = new PlayoutHistory
@ -210,7 +218,7 @@ public class BlockPlayoutBuilder( @@ -210,7 +218,7 @@ public class BlockPlayoutBuilder(
};
//logger.LogDebug("Adding history item: {When}: {History}", nextHistory.When, nextHistory.Details);
playout.PlayoutHistory.Add(nextHistory);
result.AddedHistory.Add(nextHistory);
currentTime += itemDuration;
enumerator.MoveNext();
@ -223,9 +231,9 @@ public class BlockPlayoutBuilder( @@ -223,9 +231,9 @@ public class BlockPlayoutBuilder(
}
}
CleanUpHistory(playout, start);
result = CleanUpHistory(referenceData, start, result);
return playout;
return result;
}
protected virtual async Task<int> GetDaysToBuild() =>
@ -235,6 +243,7 @@ public class BlockPlayoutBuilder( @@ -235,6 +243,7 @@ public class BlockPlayoutBuilder(
protected virtual IMediaCollectionEnumerator GetEnumerator(
Playout playout,
PlayoutReferenceData referenceData,
BlockItem blockItem,
DateTimeOffset currentTime,
string historyKey,
@ -249,27 +258,29 @@ public class BlockPlayoutBuilder( @@ -249,27 +258,29 @@ public class BlockPlayoutBuilder(
PlaybackOrder.Chronological => BlockPlayoutEnumerator.Chronological(
collectionItems,
currentTime,
playout,
referenceData.PlayoutHistory,
blockItem,
historyKey,
Logger),
PlaybackOrder.SeasonEpisode => BlockPlayoutEnumerator.SeasonEpisode(
collectionItems,
currentTime,
playout,
referenceData.PlayoutHistory,
blockItem,
historyKey,
Logger),
PlaybackOrder.Shuffle => BlockPlayoutEnumerator.Shuffle(
collectionItems,
currentTime,
playout,
playout.Seed,
referenceData.PlayoutHistory,
blockItem,
historyKey),
PlaybackOrder.RandomRotation => BlockPlayoutEnumerator.RandomRotation(
collectionItems,
currentTime,
playout,
playout.Seed,
referenceData.PlayoutHistory,
blockItem,
historyKey),
_ => new RandomizedMediaCollectionEnumerator(
@ -297,10 +308,10 @@ public class BlockPlayoutBuilder( @@ -297,10 +308,10 @@ public class BlockPlayoutBuilder(
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
}
private static void CleanUpHistory(Playout playout, DateTimeOffset start)
private static PlayoutBuildResult CleanUpHistory(PlayoutReferenceData referenceData, DateTimeOffset start, PlayoutBuildResult result)
{
var groups = new Dictionary<string, List<PlayoutHistory>>();
foreach (PlayoutHistory history in playout.PlayoutHistory)
foreach (PlayoutHistory history in referenceData.PlayoutHistory.Append(result.AddedHistory))
{
var key = $"{history.BlockId}-{history.Key}";
if (!groups.TryGetValue(key, out List<PlayoutHistory> group))
@ -323,9 +334,18 @@ public class BlockPlayoutBuilder( @@ -323,9 +334,18 @@ public class BlockPlayoutBuilder(
foreach (PlayoutHistory delete in toDelete)
{
playout.PlayoutHistory.Remove(delete);
if (delete.Id > 0)
{
result.HistoryToRemove.Add(delete.Id);
}
else
{
result.AddedHistory.Remove(delete);
}
}
}
return result;
}
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(

20
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutChangeDetection.cs

@ -8,10 +8,10 @@ namespace ErsatzTV.Core.Scheduling.BlockScheduling; @@ -8,10 +8,10 @@ namespace ErsatzTV.Core.Scheduling.BlockScheduling;
internal static class BlockPlayoutChangeDetection
{
public static Dictionary<PlayoutItem, BlockKey> GetPlayoutItemToBlockKeyMap(Playout playout)
public static Dictionary<PlayoutItem, BlockKey> GetPlayoutItemToBlockKeyMap(PlayoutReferenceData referenceData)
{
var itemBlockKeys = new Dictionary<PlayoutItem, BlockKey>();
foreach (PlayoutItem item in playout.Items)
foreach (PlayoutItem item in referenceData.ExistingItems)
{
if (!string.IsNullOrWhiteSpace(item.BlockKey))
{
@ -46,8 +46,7 @@ internal static class BlockPlayoutChangeDetection @@ -46,8 +46,7 @@ internal static class BlockPlayoutChangeDetection
{
foreach (PlayoutItem playoutItem in playoutItems)
{
int blockId = itemBlockKeys[playoutItem].b;
if (effectiveBlock.Block.Id != blockId)
if (!itemBlockKeys.TryGetValue(playoutItem, out var blockKey) || effectiveBlock.Block.Id != blockKey.b)
{
continue;
}
@ -127,7 +126,10 @@ internal static class BlockPlayoutChangeDetection @@ -127,7 +126,10 @@ internal static class BlockPlayoutChangeDetection
// find affected playout items
foreach (PlayoutItem item in playoutItems)
{
BlockKey blockKey = itemBlockKeys[item];
if (!itemBlockKeys.TryGetValue(item, out BlockKey blockKey))
{
continue;
}
bool blockKeyIsAffected = earliestEffectiveBlocks.TryGetValue(blockKey, out DateTimeOffset value) &&
value <= item.StartOffset;
@ -144,16 +146,18 @@ internal static class BlockPlayoutChangeDetection @@ -144,16 +146,18 @@ internal static class BlockPlayoutChangeDetection
return Tuple(updatedBlocks.ToList(), playoutItems.Filter(i => updatedItemIds.Contains(i.Id)).ToList());
}
public static void RemoveItemAndHistory(Playout playout, PlayoutItem playoutItem)
public static PlayoutBuildResult RemoveItemAndHistory(Playout playout, PlayoutItem playoutItem, PlayoutBuildResult result)
{
playout.Items.Remove(playoutItem);
result.ItemsToRemove.Add(playoutItem.Id);
Option<PlayoutHistory> historyToRemove = playout.PlayoutHistory
.Find(h => h.When == playoutItem.Start);
foreach (PlayoutHistory history in historyToRemove)
{
playout.PlayoutHistory.Remove(history);
result.HistoryToRemove.Add(history.Id);
}
return result;
}
}

29
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs

@ -10,13 +10,13 @@ public static class BlockPlayoutEnumerator @@ -10,13 +10,13 @@ public static class BlockPlayoutEnumerator
public static IMediaCollectionEnumerator Chronological(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
Playout playout,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey,
ILogger logger)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
Option<PlayoutHistory> maybeHistory = playoutHistory
.Filter(h => h.BlockId == blockItem.BlockId)
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
@ -45,13 +45,13 @@ public static class BlockPlayoutEnumerator @@ -45,13 +45,13 @@ public static class BlockPlayoutEnumerator
public static IMediaCollectionEnumerator SeasonEpisode(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
Playout playout,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey,
ILogger logger)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
Option<PlayoutHistory> maybeHistory = playoutHistory
.Filter(h => h.BlockId == blockItem.BlockId)
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
@ -80,19 +80,20 @@ public static class BlockPlayoutEnumerator @@ -80,19 +80,20 @@ public static class BlockPlayoutEnumerator
public static IMediaCollectionEnumerator Shuffle(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
Playout playout,
int playoutSeed,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
Option<PlayoutHistory> maybeHistory = playoutHistory
.Filter(h => h.BlockId == blockItem.BlockId)
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
var state = new CollectionEnumeratorState { Seed = playout.Seed + blockItem.BlockId, Index = 0 };
var state = new CollectionEnumeratorState { Seed = playoutSeed + blockItem.BlockId, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
state.Index = h.Index + 1;
@ -111,18 +112,19 @@ public static class BlockPlayoutEnumerator @@ -111,18 +112,19 @@ public static class BlockPlayoutEnumerator
public static IMediaCollectionEnumerator Shuffle(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
Playout playout,
int playoutSeed,
List<PlayoutHistory> playoutHistory,
Deco deco,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
Option<PlayoutHistory> maybeHistory = playoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
var state = new CollectionEnumeratorState { Seed = playout.Seed + deco.Id, Index = 0 };
var state = new CollectionEnumeratorState { Seed = playoutSeed + deco.Id, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
state.Index = h.Index + 1;
@ -141,19 +143,20 @@ public static class BlockPlayoutEnumerator @@ -141,19 +143,20 @@ public static class BlockPlayoutEnumerator
public static IMediaCollectionEnumerator RandomRotation(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
Playout playout,
int playoutSeed,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
Option<PlayoutHistory> maybeHistory = playoutHistory
.Filter(h => h.BlockId == blockItem.BlockId)
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
var state = new CollectionEnumeratorState { Seed = playout.Seed + blockItem.BlockId, Index = 0 };
var state = new CollectionEnumeratorState { Seed = playoutSeed + blockItem.BlockId, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
// Make sure to only increase the index by 1 since we can only

34
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs

@ -20,25 +20,30 @@ public class BlockPlayoutFillerBuilder( @@ -20,25 +20,30 @@ public class BlockPlayoutFillerBuilder(
NullValueHandling = NullValueHandling.Ignore
};
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
public async Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutBuildMode mode,
CancellationToken cancellationToken)
{
if (mode is PlayoutBuildMode.Reset)
{
// remove all playout items with type filler
// except block items that are hidden from the guide (guide mode)
var toRemove = playout.Items
var toRemove = result.AddedItems
.Where(pi => pi.FillerKind is not FillerKind.None and not FillerKind.GuideMode)
.ToList();
foreach (PlayoutItem playoutItem in toRemove)
{
BlockPlayoutChangeDetection.RemoveItemAndHistory(playout, playoutItem);
result = BlockPlayoutChangeDetection.RemoveItemAndHistory(playout, playoutItem, result);
}
}
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
// find all unscheduled periods
var queue = new Queue<PlayoutItem>(playout.Items);
var queue = new Queue<PlayoutItem>(result.AddedItems);
while (queue.Count > 1)
{
PlayoutItem one = queue.Dequeue();
@ -53,7 +58,7 @@ public class BlockPlayoutFillerBuilder( @@ -53,7 +58,7 @@ public class BlockPlayoutFillerBuilder(
}
// find applicable deco
foreach (Deco deco in GetDecoFor(playout, start))
foreach (Deco deco in GetDecoFor(referenceData, start))
{
if (!HasDefaultFiller(deco))
{
@ -75,7 +80,8 @@ public class BlockPlayoutFillerBuilder( @@ -75,7 +80,8 @@ public class BlockPlayoutFillerBuilder(
enumerator = BlockPlayoutEnumerator.Shuffle(
collectionItems,
start,
playout,
playout.Seed,
result.AddedHistory,
deco,
historyKey);
@ -106,6 +112,7 @@ public class BlockPlayoutFillerBuilder( @@ -106,6 +112,7 @@ public class BlockPlayoutFillerBuilder(
// add filler from deco to unscheduled period
var filler = new PlayoutItem
{
PlayoutId = playout.Id,
MediaItemId = mediaItem.Id,
Start = current.UtcDateTime,
Finish = current.UtcDateTime + itemDuration,
@ -135,7 +142,7 @@ public class BlockPlayoutFillerBuilder( @@ -135,7 +142,7 @@ public class BlockPlayoutFillerBuilder(
}
}
playout.Items.Add(filler);
result.AddedItems.Add(filler);
// create a playout history record
var nextHistory = new PlayoutHistory
@ -148,7 +155,7 @@ public class BlockPlayoutFillerBuilder( @@ -148,7 +155,7 @@ public class BlockPlayoutFillerBuilder(
Details = HistoryDetails.ForMediaItem(mediaItem)
};
playout.PlayoutHistory.Add(nextHistory);
result.AddedHistory.Add(nextHistory);
current += itemDuration;
enumerator.MoveNext();
@ -163,12 +170,13 @@ public class BlockPlayoutFillerBuilder( @@ -163,12 +170,13 @@ public class BlockPlayoutFillerBuilder(
}
return playout;
return result;
}
private static Option<Deco> GetDecoFor(Playout playout, DateTimeOffset start)
private static Option<Deco> GetDecoFor(PlayoutReferenceData referenceData, DateTimeOffset start)
{
Option<PlayoutTemplate> maybeTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(playout.Templates, start);
Option<PlayoutTemplate> maybeTemplate =
PlayoutTemplateSelector.GetPlayoutTemplateFor(referenceData.PlayoutTemplates, start);
foreach (PlayoutTemplate template in maybeTemplate)
{
if (template.DecoTemplate is not null)
@ -181,7 +189,7 @@ public class BlockPlayoutFillerBuilder( @@ -181,7 +189,7 @@ public class BlockPlayoutFillerBuilder(
switch (decoTemplateItem.Deco.DefaultFillerMode)
{
case DecoMode.Inherit:
return Optional(playout.Deco);
return referenceData.Deco;
case DecoMode.Override:
return decoTemplateItem.Deco;
case DecoMode.Disable:
@ -193,7 +201,7 @@ public class BlockPlayoutFillerBuilder( @@ -193,7 +201,7 @@ public class BlockPlayoutFillerBuilder(
}
}
return Optional(playout.Deco);
return referenceData.Deco;
}
private static bool HasDefaultFiller(Deco deco)

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

@ -26,14 +26,15 @@ public class BlockPlayoutPreviewBuilder( @@ -26,14 +26,15 @@ public class BlockPlayoutPreviewBuilder(
protected override ILogger Logger => NullLogger.Instance;
public override async Task<Playout> Build(
public override async Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken)
{
_randomizedCollections.Add(playout.Channel.UniqueId, []);
Playout result = await base.Build(playout, mode, cancellationToken);
PlayoutBuildResult result = await base.Build(playout, referenceData, mode, cancellationToken);
_randomizedCollections.Remove(playout.Channel.UniqueId);
@ -44,6 +45,7 @@ public class BlockPlayoutPreviewBuilder( @@ -44,6 +45,7 @@ public class BlockPlayoutPreviewBuilder(
protected override IMediaCollectionEnumerator GetEnumerator(
Playout playout,
PlayoutReferenceData referenceData,
BlockItem blockItem,
DateTimeOffset currentTime,
string historyKey,
@ -51,13 +53,14 @@ public class BlockPlayoutPreviewBuilder( @@ -51,13 +53,14 @@ public class BlockPlayoutPreviewBuilder(
{
IMediaCollectionEnumerator enumerator = base.GetEnumerator(
playout,
referenceData,
blockItem,
currentTime,
historyKey,
collectionMediaItems);
var collectionKey = CollectionKey.ForBlockItem(blockItem);
if (!_randomizedCollections[playout.Channel.UniqueId].Contains(collectionKey))
if (!_randomizedCollections[referenceData.Channel.UniqueId].Contains(collectionKey))
{
enumerator.ResetState(
new CollectionEnumeratorState
@ -66,7 +69,7 @@ public class BlockPlayoutPreviewBuilder( @@ -66,7 +69,7 @@ public class BlockPlayoutPreviewBuilder(
Index = new Random().Next(collectionMediaItems[collectionKey].Count)
});
_randomizedCollections[playout.Channel.UniqueId].Add(collectionKey);
_randomizedCollections[referenceData.Channel.UniqueId].Add(collectionKey);
}
return enumerator;

17
ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Scheduling;
public record PlayoutBuildResult(
bool ClearItems,
Option<DateTimeOffset> RemoveBefore,
Option<DateTimeOffset> RemoveAfter,
List<PlayoutItem> AddedItems,
List<int> ItemsToRemove,
List<PlayoutHistory> AddedHistory,
List<int> HistoryToRemove)
{
public static PlayoutBuildResult Empty =>
new(false, Option<DateTimeOffset>.None, Option<DateTimeOffset>.None, [], [], [], []);
}

181
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -62,20 +62,26 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -62,20 +62,26 @@ public class PlayoutBuilder : IPlayoutBuilder
}
}
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
public async Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken)
{
if (playout.ProgramSchedulePlayoutType is not ProgramSchedulePlayoutType.Flood)
var result = PlayoutBuildResult.Empty;
if (playout.ProgramSchedulePlayoutType is not ProgramSchedulePlayoutType.Classic)
{
_logger.LogWarning(
"Skipping playout build with type {Type} on channel {Number} - {Name}",
playout.ProgramSchedulePlayoutType,
playout.Channel.Number,
playout.Channel.Name);
referenceData.Channel.Number,
referenceData.Channel.Name);
return playout;
return result;
}
foreach (PlayoutParameters parameters in await Validate(playout))
foreach (PlayoutParameters parameters in await Validate(playout, referenceData))
{
// for testing purposes
// if (mode == PlayoutBuildMode.Reset)
@ -84,56 +90,62 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -84,56 +90,62 @@ public class PlayoutBuilder : IPlayoutBuilder
// }
// time shift on demand channel if needed
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand && mode is not PlayoutBuildMode.Reset)
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand && mode is not PlayoutBuildMode.Reset)
{
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
}
return await Build(playout, mode, parameters, cancellationToken);
result = await Build(playout, referenceData, result, mode, parameters, cancellationToken);
}
return playout;
return result;
}
private Task<Playout> Build(
private Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutBuildMode mode,
PlayoutParameters parameters,
CancellationToken cancellationToken) =>
mode switch
{
PlayoutBuildMode.Refresh => RefreshPlayout(playout, parameters, cancellationToken),
PlayoutBuildMode.Reset => ResetPlayout(playout, parameters, cancellationToken),
_ => ContinuePlayout(playout, parameters, cancellationToken)
PlayoutBuildMode.Refresh => RefreshPlayout(playout, referenceData, result, parameters, cancellationToken),
PlayoutBuildMode.Reset => ResetPlayout(playout, referenceData, result, parameters, cancellationToken),
_ => ContinuePlayout(playout, referenceData, result, parameters, cancellationToken)
};
internal async Task<Playout> Build(
internal async Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutBuildMode mode,
DateTimeOffset start,
DateTimeOffset finish,
CancellationToken cancellationToken)
{
foreach (PlayoutParameters parameters in await Validate(playout))
foreach (PlayoutParameters parameters in await Validate(playout, referenceData))
{
return await Build(playout, mode, parameters with { Start = start, Finish = finish }, cancellationToken);
result = await Build(playout, referenceData, result, mode, parameters with { Start = start, Finish = finish }, cancellationToken);
}
return playout;
return result;
}
private async Task<Playout> RefreshPlayout(
private async Task<PlayoutBuildResult> RefreshPlayout(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutParameters parameters,
CancellationToken cancellationToken)
{
_logger.LogDebug(
"Refreshing playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
referenceData.Channel.Number,
referenceData.Channel.Name);
playout.Items.Clear();
result = result with { ClearItems = true };
playout.Anchor = null;
// foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
@ -227,6 +239,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -227,6 +239,8 @@ public class PlayoutBuilder : IPlayoutBuilder
return await BuildPlayoutItems(
playout,
referenceData,
result,
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems,
@ -234,30 +248,34 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -234,30 +248,34 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken);
}
private async Task<Playout> ResetPlayout(
private async Task<PlayoutBuildResult> ResetPlayout(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutParameters parameters,
CancellationToken cancellationToken)
{
_logger.LogDebug(
"Resetting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
referenceData.Channel.Number,
referenceData.Channel.Name);
playout.Items.Clear();
result = result with { ClearItems = true };
playout.Anchor = null;
playout.ProgramScheduleAnchors.Clear();
playout.OnDemandCheckpoint = null;
// don't trim start for on demand channels, we want to time shift it all forward
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
TrimStart = false;
}
await BuildPlayoutItems(
playout,
referenceData,
result,
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems,
@ -265,24 +283,26 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -265,24 +283,26 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken);
// time shift on demand channel if needed
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
}
return playout;
return result;
}
private async Task<Playout> ContinuePlayout(
private async Task<PlayoutBuildResult> ContinuePlayout(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
PlayoutParameters parameters,
CancellationToken cancellationToken)
{
_logger.LogDebug(
"Building playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
referenceData.Channel.Number,
referenceData.Channel.Name);
// remove old checkpoints
playout.ProgramScheduleAnchors.RemoveAll(a =>
@ -290,23 +310,23 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -290,23 +310,23 @@ public class PlayoutBuilder : IPlayoutBuilder
// _logger.LogDebug("Remaining anchors: {@Anchors}", playout.ProgramScheduleAnchors);
await BuildPlayoutItems(
return await BuildPlayoutItems(
playout,
referenceData,
result,
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems,
false,
cancellationToken);
return playout;
}
private async Task<Option<PlayoutParameters>> Validate(Playout playout)
private async Task<Option<PlayoutParameters>> Validate(Playout playout, PlayoutReferenceData referenceData)
{
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(playout);
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(referenceData);
if (collectionMediaItems.IsEmpty)
{
_logger.LogWarning("Playout {Playout} has no items", playout.Channel.Name);
_logger.LogWarning("Playout {Playout} has no items", referenceData.Channel.Name);
return None;
}
@ -340,9 +360,6 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -340,9 +360,6 @@ public class PlayoutBuilder : IPlayoutBuilder
return None;
}
playout.Items ??= [];
playout.ProgramScheduleAnchors ??= [];
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
DateTimeOffset now = DateTimeOffset.Now;
@ -353,8 +370,10 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -353,8 +370,10 @@ public class PlayoutBuilder : IPlayoutBuilder
collectionMediaItems);
}
private async Task<Playout> BuildPlayoutItems(
private async Task<PlayoutBuildResult> BuildPlayoutItems(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
DateTimeOffset playoutStart,
DateTimeOffset playoutFinish,
Map<CollectionKey, List<MediaItem>> collectionMediaItems,
@ -377,9 +396,16 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -377,9 +396,16 @@ public class PlayoutBuilder : IPlayoutBuilder
// build each day with "continue" anchors
while (finish < playoutFinish)
{
if (cancellationToken.IsCancellationRequested)
{
return result;
}
_logger.LogDebug("Building playout from {Start} to {Finish}", start, finish);
playout = await BuildPlayoutItems(
result = await BuildPlayoutItems(
playout,
referenceData,
result,
start,
finish,
collectionMediaItems,
@ -394,12 +420,19 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -394,12 +420,19 @@ public class PlayoutBuilder : IPlayoutBuilder
finish = finish.AddDays(1);
}
if (cancellationToken.IsCancellationRequested)
{
return result;
}
if (start < playoutFinish)
{
// build one final time without continue anchors
_logger.LogDebug("Building final playout from {Start} to {Finish}", start, playoutFinish);
playout = await BuildPlayoutItems(
result = await BuildPlayoutItems(
playout,
referenceData,
result,
start,
playoutFinish,
collectionMediaItems,
@ -411,16 +444,16 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -411,16 +444,16 @@ public class PlayoutBuilder : IPlayoutBuilder
if (TrimStart)
{
// remove old items
playout.Items.RemoveAll(old => old.FinishOffset < trimBefore);
result = result with { RemoveBefore = trimBefore };
}
// on demand channels end up with slightly more than expected due to time shifting from midnight to first build
if (playout.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand)
if (referenceData.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand)
{
// check for future items that aren't grouped inside range
var futureItems = playout.Items.Filter(i => i.StartOffset > trimAfter).ToList();
var futureItems = result.AddedItems.Filter(i => i.StartOffset > trimAfter).ToList();
var futureItemCount = futureItems.Count(futureItem =>
playout.Items.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup));
result.AddedItems.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup));
// it feels hacky to have to clean up a playlist like this,
// so only log the warning, and leave the bad data to fail tests
@ -435,11 +468,13 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -435,11 +468,13 @@ public class PlayoutBuilder : IPlayoutBuilder
}
}
return playout;
return result;
}
private async Task<Playout> BuildPlayoutItems(
private async Task<PlayoutBuildResult> BuildPlayoutItems(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildResult result,
DateTimeOffset playoutStart,
DateTimeOffset playoutFinish,
Map<CollectionKey, List<MediaItem>> collectionMediaItems,
@ -448,21 +483,21 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -448,21 +483,21 @@ public class PlayoutBuilder : IPlayoutBuilder
CancellationToken cancellationToken)
{
ProgramSchedule activeSchedule = PlayoutScheduleSelector.GetProgramScheduleFor(
playout.ProgramSchedule,
playout.ProgramScheduleAlternates,
referenceData.ProgramSchedule,
referenceData.ProgramScheduleAlternates,
playoutStart);
if (activeSchedule.Items.Count == 0)
{
// empty schedule results in empty day
playout.Anchor = new PlayoutAnchor { NextStart = playoutFinish.UtcDateTime };
return playout;
return result;
}
// on demand channels do NOT use alternate schedules
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
activeSchedule = playout.ProgramSchedule;
activeSchedule = referenceData.ProgramSchedule;
}
// _logger.LogDebug("Active schedule is: {Schedule}", activeSchedule.Name);
@ -622,7 +657,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -622,7 +657,7 @@ public class PlayoutBuilder : IPlayoutBuilder
if (currentTime >= playoutFinish)
{
// nothing to do, no need to add more anchors
return playout;
return result;
}
// _logger.LogDebug(
@ -635,15 +670,12 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -635,15 +670,12 @@ public class PlayoutBuilder : IPlayoutBuilder
// removing any items scheduled past the start anchor
// this could happen if the app was closed after scheduling items
// but before saving the anchor
int removed = playout.Items.RemoveAll(pi => pi.StartOffset >= currentTime);
if (removed > 0)
{
_logger.LogWarning("Removed {Count} schedule items beyond current start anchor", removed);
}
result = result with { RemoveAfter = currentTime };
// start with the previously-decided schedule item
// start with the previous multiple/duration states
var playoutBuilderState = new PlayoutBuilderState(
playout.Id,
scheduleItemsEnumerator,
Optional(startAnchor.MultipleRemaining),
startAnchor.DurationFinishOffset,
@ -693,7 +725,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -693,7 +725,7 @@ public class PlayoutBuilder : IPlayoutBuilder
ProgramScheduleItem nextScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Peek(1);
Tuple<PlayoutBuilderState, List<PlayoutItem>> result = scheduleItem switch
Tuple<PlayoutBuilderState, List<PlayoutItem>> schedulerResult = scheduleItem switch
{
ProgramScheduleItemMultiple multiple => schedulerMultiple.Schedule(
playoutBuilderState,
@ -726,7 +758,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -726,7 +758,7 @@ public class PlayoutBuilder : IPlayoutBuilder
_ => throw new NotSupportedException(nameof(scheduleItem))
};
(PlayoutBuilderState nextState, List<PlayoutItem> playoutItems) = result;
(PlayoutBuilderState nextState, List<PlayoutItem> playoutItems) = schedulerResult;
// if we completed a multiple/duration block, move to the next fill group
if (scheduleItem.FillWithGroupMode is not FillWithGroupMode.None)
@ -737,10 +769,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -737,10 +769,7 @@ public class PlayoutBuilder : IPlayoutBuilder
}
}
foreach (PlayoutItem playoutItem in playoutItems)
{
playout.Items.Add(playoutItem);
}
result.AddedItems.AddRange(playoutItems);
playoutBuilderState = nextState;
}
@ -748,9 +777,9 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -748,9 +777,9 @@ public class PlayoutBuilder : IPlayoutBuilder
// once more to get playout anchor
ProgramScheduleItem anchorScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
if (playout.Items.Count != 0)
if (result.AddedItems.Count != 0)
{
DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset);
DateTimeOffset maxStartTime = result.AddedItems.Max(i => i.FinishOffset);
if (maxStartTime < playoutBuilderState.CurrentTime)
{
playoutBuilderState = playoutBuilderState with { CurrentTime = maxStartTime };
@ -783,8 +812,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -783,8 +812,8 @@ public class PlayoutBuilder : IPlayoutBuilder
}
ProgramSchedule activeScheduleAtAnchor = PlayoutScheduleSelector.GetProgramScheduleFor(
playout.ProgramSchedule,
playout.ProgramScheduleAlternates,
referenceData.ProgramSchedule,
referenceData.ProgramScheduleAlternates,
playoutBuilderState.CurrentTime);
// if we ended in a different alternate schedule, fix the anchor data
@ -818,7 +847,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -818,7 +847,7 @@ public class PlayoutBuilder : IPlayoutBuilder
// build fill group indices
playout.FillGroupIndices = BuildFillGroupIndices(playout, scheduleItemsFillGroupEnumerators);
return playout;
return result;
}
private static List<PlayoutScheduleItemFillGroupIndex> BuildFillGroupIndices(
@ -853,9 +882,9 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -853,9 +882,9 @@ public class PlayoutBuilder : IPlayoutBuilder
return result;
}
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(Playout playout)
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(PlayoutReferenceData referenceData)
{
IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> collectionKeys = GetAllCollectionKeys(playout);
IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> collectionKeys = GetAllCollectionKeys(referenceData);
IEnumerable<Task<KeyValuePair<CollectionKey, List<MediaItem>>>> tasks = collectionKeys.Select(async key =>
{
@ -866,10 +895,10 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -866,10 +895,10 @@ public class PlayoutBuilder : IPlayoutBuilder
return Map.createRange(await Task.WhenAll(tasks));
}
private static IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> GetAllCollectionKeys(Playout playout)
private static IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> GetAllCollectionKeys(PlayoutReferenceData referenceData)
{
return playout.ProgramSchedule.Items
.Append(playout.ProgramScheduleAlternates.Bind(psa => psa.ProgramSchedule.Items))
return referenceData.ProgramSchedule.Items
.Append(referenceData.ProgramScheduleAlternates.Bind(psa => psa.ProgramSchedule.Items))
.DistinctBy(item => item.Id)
.SelectMany(CollectionKeysForItem)
.DistinctBy(kvp => kvp.Key);

1
ErsatzTV.Core/Scheduling/PlayoutBuilderState.cs

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
namespace ErsatzTV.Core.Scheduling;
public record PlayoutBuilderState(
int PlayoutId,
IScheduleItemsEnumerator ScheduleItemsEnumerator,
Option<int> MultipleRemaining,
Option<DateTimeOffset> DurationFinish,

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -132,6 +132,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -132,6 +132,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = IdForMediaItem(mediaItem),
Start = nextState.CurrentTime.UtcDateTime,
Finish = nextState.CurrentTime.UtcDateTime + itemDuration,
@ -177,6 +178,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -177,6 +178,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
{
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = mediaItem.Id,
Start = nextState.CurrentTime.UtcDateTime,
Finish = nextItemStart.UtcDateTime,
@ -753,6 +755,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -753,6 +755,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = IdForMediaItem(mediaItem),
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + itemDuration,
@ -797,6 +800,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -797,6 +800,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = IdForMediaItem(mediaItem),
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + itemDuration,
@ -846,6 +850,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -846,6 +850,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
{
var result = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = mediaItem.Id,
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + duration,

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -146,6 +146,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche @@ -146,6 +146,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -65,6 +65,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul @@ -65,6 +65,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -89,6 +89,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -89,6 +89,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -36,6 +36,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI @@ -36,6 +36,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
var playoutItem = new PlayoutItem
{
PlayoutId = playoutBuilderState.PlayoutId,
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,

13
ErsatzTV.Core/Scheduling/PlayoutReferenceData.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Scheduling;
public record PlayoutReferenceData(
Channel Channel,
Option<Deco> Deco,
List<PlayoutItem> ExistingItems,
List<PlayoutTemplate> PlayoutTemplates,
ProgramSchedule ProgramSchedule,
List<ProgramScheduleAlternate> ProgramScheduleAlternates,
List<PlayoutHistory> PlayoutHistory);

3
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs

@ -46,6 +46,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -46,6 +46,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
// create a playout item
var playoutItem = new PlayoutItem
{
PlayoutId = context.Playout.Id,
MediaItemId = mediaItem.Id,
Start = context.CurrentTime.UtcDateTime,
Finish = context.CurrentTime.UtcDateTime + itemDuration,
@ -91,7 +92,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -91,7 +92,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
context.AddedHistory.Add(history);
}
enumerator.MoveNext();

5
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache)
{
public async Task<bool> Handle(
PlayoutReferenceData referenceData,
YamlPlayoutContext context,
YamlPlayoutContentItem contentItem,
ILogger<YamlPlayoutBuilder> logger,
@ -38,7 +39,7 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) @@ -38,7 +39,7 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache)
string historyKey = HistoryDetails.KeyForYamlContent(contentItem);
DateTime historyTime = context.CurrentTime.UtcDateTime;
Option<DateTime> maxWhen = await context.Playout.PlayoutHistory
Option<DateTime> maxWhen = await referenceData.PlayoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.Map(h => h.When)
@ -46,7 +47,7 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) @@ -46,7 +47,7 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache)
.HeadOrNone()
.IfNoneAsync(DateTime.MinValue);
var maybeHistory = context.Playout.PlayoutHistory
var maybeHistory = referenceData.PlayoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When == maxWhen)
.ToList();

6
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs

@ -180,7 +180,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -180,7 +180,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
Option<YamlPlayoutContext.MidRollSequence> maybeMidRollSequence = context.GetMidRollSequence();
if (itemChapters.Count < 2 || maybeMidRollSequence.IsNone)
{
context.Playout.Items.Add(playoutItem);
context.AddedItems.Add(playoutItem);
context.CurrentTime += playoutItem.OutPoint - playoutItem.InPoint;
}
else
@ -194,7 +194,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -194,7 +194,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
if (filteredChapters.Count < 2)
{
context.Playout.Items.Add(playoutItem);
context.AddedItems.Add(playoutItem);
context.CurrentTime += playoutItem.OutPoint - playoutItem.InPoint;
}
else
@ -205,7 +205,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -205,7 +205,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
nextItem.Start = context.CurrentTime.UtcDateTime;
nextItem.Finish = context.CurrentTime.UtcDateTime + (nextItem.OutPoint - nextItem.InPoint);
context.Playout.Items.Add(nextItem);
context.AddedItems.Add(nextItem);
context.CurrentTime += nextItem.OutPoint - nextItem.InPoint;

3
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs

@ -71,6 +71,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -71,6 +71,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
// create a playout item
var playoutItem = new PlayoutItem
{
PlayoutId = context.Playout.Id,
MediaItemId = mediaItem.Id,
Start = context.CurrentTime.UtcDateTime,
Finish = context.CurrentTime.UtcDateTime + itemDuration,
@ -116,7 +117,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -116,7 +117,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
context.AddedHistory.Add(history);
}
enumerator.MoveNext();

13
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs

@ -114,6 +114,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -114,6 +114,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
var playoutItem = new PlayoutItem
{
PlayoutId = context.Playout.Id,
MediaItemId = mediaItem.Id,
Start = context.CurrentTime.UtcDateTime,
Finish = context.CurrentTime.UtcDateTime + itemDuration,
@ -138,7 +139,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -138,7 +139,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
if (remainingToFill - itemDuration >= TimeSpan.Zero || !stopBeforeEnd)
{
context.Playout.Items.Add(playoutItem);
context.AddedItems.Add(playoutItem);
context.AdvanceGuideGroup();
// create history record
@ -152,7 +153,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -152,7 +153,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
context.AddedHistory.Add(history);
}
remainingToFill -= itemDuration;
@ -172,7 +173,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -172,7 +173,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start;
context.Playout.Items.Add(playoutItem);
context.AddedItems.Add(playoutItem);
context.AdvanceGuideGroup();
// create history record
@ -186,7 +187,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -186,7 +187,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
context.AddedHistory.Add(history);
}
remainingToFill = TimeSpan.Zero;
@ -209,7 +210,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -209,7 +210,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.FillerKind = FillerKind.Fallback;
context.Playout.Items.Add(playoutItem);
context.AddedItems.Add(playoutItem);
// create history record
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
@ -222,7 +223,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -222,7 +223,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
context.AddedHistory.Add(history);
}
fallback.MoveNext();

4
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs

@ -22,13 +22,13 @@ public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler @@ -22,13 +22,13 @@ public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler
return Task.FromResult(false);
}
if (context.VisitedAll && _itemsSinceLastRepeat == context.Playout.Items.Count)
if (context.VisitedAll && _itemsSinceLastRepeat == context.AddedItems.Count)
{
logger.LogWarning("Repeat encountered without adding any playout items; aborting");
throw new InvalidOperationException("YAML playout loop detected");
}
_itemsSinceLastRepeat = context.Playout.Items.Count;
_itemsSinceLastRepeat = context.AddedItems.Count;
context.InstructionIndex = 0;
return Task.FromResult(true);
}

49
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs

@ -23,12 +23,18 @@ public class YamlPlayoutBuilder( @@ -23,12 +23,18 @@ public class YamlPlayoutBuilder(
ILogger<YamlPlayoutBuilder> logger)
: IYamlPlayoutBuilder
{
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
public async Task<PlayoutBuildResult> Build(
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,
CancellationToken cancellationToken)
{
var result = PlayoutBuildResult.Empty;
if (!localFileSystem.FileExists(playout.TemplateFile))
{
logger.LogWarning("YAML playout file {File} does not exist; aborting.", playout.TemplateFile);
return playout;
return result;
}
Option<YamlPlayoutDefinition> maybePlayoutDefinition =
@ -36,7 +42,7 @@ public class YamlPlayoutBuilder( @@ -36,7 +42,7 @@ public class YamlPlayoutBuilder(
if (maybePlayoutDefinition.IsNone)
{
logger.LogWarning("YAML playout file {File} is invalid; aborting.", playout.TemplateFile);
return playout;
return result;
}
// using ValueUnsafe to avoid nesting
@ -55,7 +61,7 @@ public class YamlPlayoutBuilder( @@ -55,7 +61,7 @@ public class YamlPlayoutBuilder(
if (!File.Exists(path))
{
logger.LogError("YAML playout import {File} does not exist.", path);
return playout;
return result;
}
}
@ -76,7 +82,7 @@ public class YamlPlayoutBuilder( @@ -76,7 +82,7 @@ public class YamlPlayoutBuilder(
if (maybeImportedDefinition.IsNone)
{
logger.LogWarning("YAML playout import {File} is invalid; aborting.", import);
return playout;
return result;
}
}
catch (Exception ex)
@ -109,7 +115,7 @@ public class YamlPlayoutBuilder( @@ -109,7 +115,7 @@ public class YamlPlayoutBuilder(
// remove old items
// importantly, this should not remove their history
playout.Items.RemoveAll(i => i.FinishOffset < start);
result = result with { RemoveBefore = start };
// load saved state
if (mode is not PlayoutBuildMode.Reset)
@ -127,17 +133,17 @@ public class YamlPlayoutBuilder( @@ -127,17 +133,17 @@ public class YamlPlayoutBuilder(
// start = start.AddHours(-2);
// erase items, not history
playout.Items.Clear();
result = result with { ClearItems = true };
// remove any future or "currently active" history items
// this prevents "walking" the playout forward by repeatedly resetting
var toRemove = new List<PlayoutHistory>();
toRemove.AddRange(
playout.PlayoutHistory.Filter(h =>
referenceData.PlayoutHistory.Filter(h =>
h.When > start.UtcDateTime || h.When <= start.UtcDateTime && h.Finish >= start.UtcDateTime));
foreach (PlayoutHistory history in toRemove)
{
playout.PlayoutHistory.Remove(history);
result.HistoryToRemove.Add(history.Id);
}
}
@ -151,7 +157,7 @@ public class YamlPlayoutBuilder( @@ -151,7 +157,7 @@ public class YamlPlayoutBuilder(
var applyHistoryHandler = new YamlPlayoutApplyHistoryHandler(enumeratorCache);
foreach (YamlPlayoutContentItem contentItem in playoutDefinition.Content)
{
await applyHistoryHandler.Handle(context, contentItem, logger, cancellationToken);
await applyHistoryHandler.Handle(referenceData, context, contentItem, logger, cancellationToken);
}
if (mode is PlayoutBuildMode.Reset)
@ -189,7 +195,7 @@ public class YamlPlayoutBuilder( @@ -189,7 +195,7 @@ public class YamlPlayoutBuilder(
if (DetectCycle(context.Definition))
{
logger.LogError("YAML sequence contains a cycle; unable to build playout");
return playout;
return result;
}
var flattenCount = 0;
@ -245,12 +251,12 @@ public class YamlPlayoutBuilder( @@ -245,12 +251,12 @@ public class YamlPlayoutBuilder(
}
}
CleanUpHistory(playout, start);
result = CleanUpHistory(referenceData, start, result);
DateTime maxTime = context.CurrentTime.UtcDateTime;
if (playout.Items.Count > 0)
if (context.AddedItems.Count > 0)
{
maxTime = playout.Items.Max(i => i.Finish);
maxTime = context.AddedItems.Max(i => i.Finish);
}
var anchor = new PlayoutAnchor
@ -268,7 +274,9 @@ public class YamlPlayoutBuilder( @@ -268,7 +274,9 @@ public class YamlPlayoutBuilder(
playout.Anchor = anchor;
return playout;
result.AddedItems.AddRange(context.AddedItems);
return result;
}
private async Task ExecuteSequence(
@ -477,10 +485,13 @@ public class YamlPlayoutBuilder( @@ -477,10 +485,13 @@ public class YamlPlayoutBuilder(
}
}
private static void CleanUpHistory(Playout playout, DateTimeOffset start)
private static PlayoutBuildResult CleanUpHistory(
PlayoutReferenceData referenceData,
DateTimeOffset start,
PlayoutBuildResult result)
{
var groups = new Dictionary<string, List<PlayoutHistory>>();
foreach (PlayoutHistory history in playout.PlayoutHistory)
foreach (PlayoutHistory history in referenceData.PlayoutHistory)
{
if (!groups.TryGetValue(history.Key, out List<PlayoutHistory> group))
{
@ -509,9 +520,11 @@ public class YamlPlayoutBuilder( @@ -509,9 +520,11 @@ public class YamlPlayoutBuilder(
foreach (PlayoutHistory delete in toDelete)
{
playout.PlayoutHistory.Remove(delete);
result.HistoryToRemove.Add(delete.Id);
}
}
}
return result;
}
}

5
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Newtonsoft.Json;
@ -24,6 +25,10 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio @@ -24,6 +25,10 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
public Playout Playout { get; } = playout;
public List<PlayoutItem> AddedItems { get; } = [];
public List<PlayoutHistory> AddedHistory { get; } = [];
public YamlPlayoutDefinition Definition { get; } = definition;
public DateTimeOffset CurrentTime { get; set; }

1
ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="EFCore.BulkExtensions.MySql" Version="8.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-rc.1.efcore.9.0.0" />
</ItemGroup>

1
ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="EFCore.BulkExtensions.Sqlite" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
</ItemGroup>

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -88,6 +88,7 @@ public class TvContext : DbContext @@ -88,6 +88,7 @@ public class TvContext : DbContext
public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
public DbSet<ProgramScheduleItem> ProgramScheduleItems { get; set; }
public DbSet<Playout> Playouts { get; set; }
public DbSet<PlayoutHistory> PlayoutHistory { get; set; }
public DbSet<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
public DbSet<PlayoutItem> PlayoutItems { get; set; }
public DbSet<PlayoutProgramScheduleAnchor> PlayoutProgramScheduleItemAnchors { get; set; }

1
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.7" />
<PackageReference Include="Jint" Version="4.4.1" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />

4
ErsatzTV/Pages/Playouts.razor

@ -111,7 +111,7 @@ @@ -111,7 +111,7 @@
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
}
</div>
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
@if (context.PlayoutType == ProgramSchedulePlayoutType.Classic)
{
if (context.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
@ -298,7 +298,7 @@ @@ -298,7 +298,7 @@
private async Task PlayoutSelected(PlayoutNameViewModel playout)
{
// only show details for flood, block and YAML playouts
_selectedPlayoutId = playout.PlayoutType is ProgramSchedulePlayoutType.Flood or ProgramSchedulePlayoutType.Block or ProgramSchedulePlayoutType.Yaml
_selectedPlayoutId = playout.PlayoutType is ProgramSchedulePlayoutType.Classic or ProgramSchedulePlayoutType.Block or ProgramSchedulePlayoutType.Yaml
? playout.PlayoutId
: null;

Loading…
Cancel
Save