Browse Source

add yaml playout history; allow yaml playouts to be extended (#1832)

* add multi_part; refactor skipping items

* save and apply history for yaml playouts

* do not remove history on yaml playout reset
pull/1834/head
Jason Dove 10 months ago committed by GitHub
parent
commit
7d83e66ba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      ErsatzTV.Application/Playouts/Commands/CreateYamlPlayoutHandler.cs
  2. 3
      ErsatzTV.Application/Playouts/Queries/GetPlayoutById.cs
  3. 32
      ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs
  4. 3
      ErsatzTV.Application/Scheduling/Commands/EraseBlockPlayoutHistory.cs
  5. 3
      ErsatzTV.Application/Scheduling/Commands/EraseBlockPlayoutItems.cs
  6. 3
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistory.cs
  7. 10
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs
  8. 3
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItems.cs
  9. 10
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItemsHandler.cs
  10. 1
      ErsatzTV.Core/Domain/PlayoutAnchor.cs
  11. 3
      ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs
  12. 14
      ErsatzTV.Core/Scheduling/HistoryDetails.cs
  13. 15
      ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs
  14. 14
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs
  15. 66
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs
  16. 37
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  17. 14
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  18. 58
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  19. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs
  20. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs
  21. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs
  22. 28
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs
  23. 1
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipToItemHandler.cs
  24. 5
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs
  25. 130
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  26. 18
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs
  27. 5866
      ErsatzTV.Infrastructure.MySql/Migrations/20240729175905_Add_PlayoutHistory_Finish.Designer.cs
  28. 41
      ErsatzTV.Infrastructure.MySql/Migrations/20240729175905_Add_PlayoutHistory_Finish.cs
  29. 6
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  30. 5705
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240729175656_Add_PlayoutHistory_Finish.Designer.cs
  31. 41
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240729175656_Add_PlayoutHistory_Finish.cs
  32. 6
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  33. 2
      ErsatzTV/Pages/BlockPlayoutEditor.razor
  34. 2
      ErsatzTV/Pages/PlayoutEditor.razor
  35. 23
      ErsatzTV/Pages/Playouts.razor
  36. 122
      ErsatzTV/Pages/YamlPlayoutEditor.razor

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

@ -55,7 +55,8 @@ public class CreateYamlPlayoutHandler @@ -55,7 +55,8 @@ public class CreateYamlPlayoutHandler
{
ChannelId = channel.Id,
TemplateFile = externalJsonFile,
ProgramSchedulePlayoutType = playoutType
ProgramSchedulePlayoutType = playoutType,
Seed = new Random().Next()
});
private static Task<Validation<BaseError, Channel>> ValidateChannel(

3
ErsatzTV.Application/Playouts/Queries/GetPlayoutById.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Playouts;
public record GetPlayoutById(int PlayoutId) : IRequest<Option<PlayoutNameViewModel>>;

32
ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetPlayoutById, Option<PlayoutNameViewModel>>
{
public async Task<Option<PlayoutNameViewModel>> Handle(
GetPlayoutById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.AsNoTracking()
.Include(p => p.ProgramSchedule)
.Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
.MapT(
p => new PlayoutNameViewModel(
p.Id,
p.ProgramSchedulePlayoutType,
p.Channel.Name,
p.Channel.Number,
p.Channel.ProgressMode,
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
p.TemplateFile,
p.ExternalJsonFile,
p.DailyRebuildTime));
}
}

3
ErsatzTV.Application/Scheduling/Commands/EraseBlockPlayoutHistory.cs

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

3
ErsatzTV.Application/Scheduling/Commands/EraseBlockPlayoutItems.cs

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

3
ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistory.cs

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

10
ErsatzTV.Application/Scheduling/Commands/EraseBlockPlayoutHistoryHandler.cs → ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs

@ -5,15 +5,17 @@ using Microsoft.EntityFrameworkCore; @@ -5,15 +5,17 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class EraseBlockPlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<EraseBlockPlayoutHistory>
public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<ErasePlayoutHistory>
{
public async Task Handle(EraseBlockPlayoutHistory request, CancellationToken cancellationToken)
public async Task Handle(ErasePlayoutHistory request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Filter(p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block)
.Filter(
p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block ||
p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout)

3
ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItems.cs

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

10
ErsatzTV.Application/Scheduling/Commands/EraseBlockPlayoutItemsHandler.cs → ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItemsHandler.cs

@ -6,17 +6,19 @@ using Microsoft.EntityFrameworkCore; @@ -6,17 +6,19 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class EraseBlockPlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<EraseBlockPlayoutItems>
public class ErasePlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<ErasePlayoutItems>
{
public async Task Handle(EraseBlockPlayoutItems request, CancellationToken cancellationToken)
public async Task Handle(ErasePlayoutItems request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Items)
.Include(p => p.PlayoutHistory)
.Filter(p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block)
.Filter(
p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block ||
p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout)

1
ErsatzTV.Core/Domain/PlayoutAnchor.cs

@ -9,6 +9,7 @@ public class PlayoutAnchor @@ -9,6 +9,7 @@ public class PlayoutAnchor
public bool InFlood { get; set; }
public bool InDurationFiller { get; set; }
public int NextGuideGroup { get; set; }
public int NextInstructionIndex { get; set; }
public DateTimeOffset NextStartOffset => new DateTimeOffset(NextStart, TimeSpan.Zero).ToLocalTime();

3
ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs

@ -18,6 +18,9 @@ public class PlayoutHistory @@ -18,6 +18,9 @@ public class PlayoutHistory
// last occurence of an item from this collection in the playout
public DateTime When { get; set; }
// used to efficiently ignore/remove "still active" history items
public DateTime Finish { get; set; }
// details about the item
public string Details { get; set; }
}

14
ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs → ErsatzTV.Core/Scheduling/HistoryDetails.cs

@ -1,9 +1,10 @@ @@ -1,9 +1,10 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Newtonsoft.Json;
namespace ErsatzTV.Core.Scheduling.BlockScheduling;
namespace ErsatzTV.Core.Scheduling;
internal static class HistoryDetails
{
@ -28,6 +29,17 @@ internal static class HistoryDetails @@ -28,6 +29,17 @@ internal static class HistoryDetails
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
}
public static string KeyForYamlContent(YamlPlayoutContentItem contentItem)
{
dynamic key = new
{
contentItem.Key,
contentItem.Order
};
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
}
public static string ForDefaultFiller(Deco deco)
{
dynamic key = new

15
ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs

@ -7,10 +7,14 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling; @@ -7,10 +7,14 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepository)
{
private readonly Dictionary<string, List<MediaItem>> _mediaItems = new();
private readonly Dictionary<string, IMediaCollectionEnumerator> _enumerators = new();
public System.Collections.Generic.HashSet<string> MissingContentKeys { get; } = [];
public List<MediaItem> MediaItemsForContent(string contentKey) =>
_mediaItems.TryGetValue(contentKey, out List<MediaItem> items) ? items : [];
public async Task<Option<IMediaCollectionEnumerator>> GetCachedEnumeratorForContent(
YamlPlayoutContext context,
string contentKey,
@ -66,17 +70,18 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -66,17 +70,18 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
break;
}
// start at the appropriate place in the enumerator
context.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex);
_mediaItems[content.Key] = items;
var state = new CollectionEnumeratorState { Seed = context.Playout.Seed + index, Index = enumeratorIndex };
var state = new CollectionEnumeratorState { Seed = context.Playout.Seed + index, Index = 0 };
switch (Enum.Parse<PlaybackOrder>(content.Order, true))
{
case PlaybackOrder.Chronological:
return new ChronologicalMediaCollectionEnumerator(items, state);
case PlaybackOrder.Shuffle:
// TODO: fix this
var groupedMediaItems = items.Map(mi => new GroupedMediaItem(mi, null)).ToList();
bool keepMultiPartEpisodesTogether = content.MultiPart;
List<GroupedMediaItem> groupedMediaItems = keepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(items, treatCollectionsAsShows: false)
: items.Map(mi => new GroupedMediaItem(mi, null)).ToList();
return new ShuffledMediaCollectionEnumerator(groupedMediaItems, state, cancellationToken);
}

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
@ -57,6 +58,19 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -57,6 +58,19 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
context.Playout.Items.Add(playoutItem);
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
instruction.Content,
enumerator,
playoutItem,
mediaItem);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
context.CurrentTime += itemDuration;
enumerator.MoveNext();
}

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

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache)
{
public async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutContentItem contentItem,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(contentItem.Key))
{
return false;
}
if (!Enum.TryParse(contentItem.Order, true, out PlaybackOrder playbackOrder))
{
return false;
}
Option<IMediaCollectionEnumerator> maybeEnumerator = await enumeratorCache.GetCachedEnumeratorForContent(
context,
contentItem.Key,
cancellationToken);
if (maybeEnumerator.IsNone)
{
return false;
}
// check for playout history for this content
string historyKey = HistoryDetails.KeyForYamlContent(contentItem);
DateTime historyTime = context.CurrentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = context.Playout.PlayoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
List<MediaItem> collectionItems = enumeratorCache.MediaItemsForContent(contentItem.Key);
// seek to the appropriate place in the collection enumerator
foreach (PlayoutHistory h in maybeHistory)
{
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
enumerator,
playbackOrder);
}
}
return true;
}
}

37
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.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.Extensions;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
@ -45,6 +46,42 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -45,6 +46,42 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
return maybeEnumerator;
}
protected static Option<PlayoutHistory> GetHistoryForItem(
YamlPlayoutContext context,
string contentKey,
IMediaCollectionEnumerator enumerator,
PlayoutItem playoutItem,
MediaItem mediaItem)
{
int index = context.Definition.Content.FindIndex(c => c.Key == contentKey);
if (index < 0)
{
return Option<PlayoutHistory>.None;
}
YamlPlayoutContentItem contentItem = context.Definition.Content[index];
if (!Enum.TryParse(contentItem.Order, true, out PlaybackOrder playbackOrder))
{
return Option<PlayoutHistory>.None;
}
string historyKey = HistoryDetails.KeyForYamlContent(contentItem);
// create a playout history record
var nextHistory = new PlayoutHistory
{
PlayoutId = context.Playout.Id,
PlaybackOrder = playbackOrder,
Index = enumerator.State.Index,
When = playoutItem.StartOffset.UtcDateTime,
Finish = playoutItem.FinishOffset.UtcDateTime,
Key = historyKey,
Details = HistoryDetails.ForMediaItem(mediaItem)
};
return nextHistory;
}
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
if (mediaItem is Image image)

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
@ -57,6 +58,19 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -57,6 +58,19 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
context.Playout.Items.Add(playoutItem);
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
instruction.Content,
enumerator,
playoutItem,
mediaItem);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
context.CurrentTime += itemDuration;
enumerator.MoveNext();
}

58
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.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.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
@ -30,7 +31,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -30,7 +31,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetContentEnumerator(
context,
instruction.Content,
duration.Content,
logger,
cancellationToken);
@ -44,6 +45,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -44,6 +45,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
{
context.CurrentTime = Schedule(
context,
instruction.Content,
duration.Fallback,
targetTime,
duration.DiscardAttempts,
duration.Trim,
@ -59,6 +62,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -59,6 +62,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
protected static DateTimeOffset Schedule(
YamlPlayoutContext context,
string contentKey,
string fallbackContentKey,
DateTimeOffset targetTime,
int discardAttempts,
bool trim,
@ -88,10 +93,25 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -88,10 +93,25 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
if (remainingToFill - itemDuration >= TimeSpan.Zero)
{
context.Playout.Items.Add(playoutItem);
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
contentKey,
enumerator,
playoutItem,
mediaItem);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
remainingToFill -= itemDuration;
context.CurrentTime += itemDuration;
context.Playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (discardAttempts > 0)
@ -103,13 +123,27 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -103,13 +123,27 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
else if (trim)
{
// trim item to exactly fit
remainingToFill = TimeSpan.Zero;
context.CurrentTime = targetTime;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start;
context.Playout.Items.Add(playoutItem);
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
contentKey,
enumerator,
playoutItem,
mediaItem);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
remainingToFill = TimeSpan.Zero;
context.CurrentTime = targetTime;
enumerator.MoveNext();
}
else if (fallbackEnumerator.IsSome)
@ -127,6 +161,20 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -127,6 +161,20 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
playoutItem.FillerKind = FillerKind.Fallback;
context.Playout.Items.Add(playoutItem);
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
fallbackContentKey,
fallback,
playoutItem,
mediaItem);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
fallback.MoveNext();
}
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs

@ -53,6 +53,8 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml @@ -53,6 +53,8 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml
{
context.CurrentTime = Schedule(
context,
padToNext.Content,
padToNext.Fallback,
targetTime,
padToNext.DiscardAttempts,
padToNext.Trim,

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs

@ -63,6 +63,8 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP @@ -63,6 +63,8 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP
{
context.CurrentTime = Schedule(
context,
padUntil.Content,
padUntil.Fallback,
targetTime,
padUntil.DiscardAttempts,
padUntil.Trim,

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

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

28
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs

@ -1,13 +1,14 @@ @@ -1,13 +1,14 @@
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler
public class YamlPlayoutSkipItemsHandler(EnumeratorCache enumeratorCache) : IYamlPlayoutHandler
{
public bool Reset => true;
public Task<bool> Handle(
public async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
@ -15,19 +16,28 @@ public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler @@ -15,19 +16,28 @@ public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler
{
if (instruction is not YamlPlayoutSkipItemsInstruction skipItems)
{
return Task.FromResult(false);
return false;
}
if (context.ContentIndex.TryGetValue(skipItems.Content, out int value))
if (skipItems.SkipItems < 0)
{
value += skipItems.SkipItems;
logger.LogWarning("Unable to skip invalid number: {Skip}", skipItems.SkipItems);
return false;
}
else
Option<IMediaCollectionEnumerator> maybeEnumerator = await enumeratorCache.GetCachedEnumeratorForContent(
context,
skipItems.Content,
cancellationToken);
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
value = skipItems.SkipItems;
for (var i = 0; i < skipItems.SkipItems; i++)
{
enumerator.MoveNext();
}
}
context.ContentIndex[skipItems.Content] = value;
return Task.FromResult(true);
return true;
}
}

1
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipToItemHandler.cs

@ -52,7 +52,6 @@ public class YamlPlayoutSkipToItemHandler(EnumeratorCache enumeratorCache) : IYa @@ -52,7 +52,6 @@ public class YamlPlayoutSkipToItemHandler(EnumeratorCache enumeratorCache) : IYa
if (episode.Season?.SeasonNumber == skipToItem.Season
&& episode.EpisodeMetadata.HeadOrNone().Map(em => em.EpisodeNumber) == skipToItem.Episode)
{
context.ContentIndex[skipToItem.Content] = index;
done = true;
break;
}

5
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs

@ -1,7 +1,12 @@ @@ -1,7 +1,12 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentItem
{
public string Key { get; set; }
public string Order { get; set; }
[YamlMember(Alias = "multi_part", ApplyNamingConventions = false)]
public bool MultiPart { get; set; }
}

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

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
@ -29,32 +30,80 @@ public class YamlPlayoutBuilder( @@ -29,32 +30,80 @@ public class YamlPlayoutBuilder(
YamlPlayoutDefinition playoutDefinition = await LoadYamlDefinition(playout, cancellationToken);
DateTimeOffset start = DateTimeOffset.Now;
int daysToBuild = await GetDaysToBuild();
DateTimeOffset finish = start.AddDays(daysToBuild);
if (mode is not PlayoutBuildMode.Reset)
{
logger.LogWarning("YAML playouts can only be reset; ignoring build mode {Mode}", mode.ToString());
return playout;
}
// load content and content enumerators on demand
Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers = new();
var enumeratorCache = new EnumeratorCache(mediaCollectionRepository);
var context = new YamlPlayoutContext(playout, playoutDefinition)
{
CurrentTime = start,
GuideGroup = 1,
InstructionIndex = 0
GuideGroup = 1
// no need to init default value and throw off visited count
// InstructionIndex = 0
};
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (mode is PlayoutBuildMode.Reset)
// logger.LogDebug(
// "Default yaml context from {Start} to {Finish}, instruction {Instruction}",
// context.CurrentTime,
// finish,
// context.InstructionIndex);
// remove old items
// importantly, this should not remove their history
playout.Items.RemoveAll(i => i.FinishOffset < start);
// load saved state
if (mode is not PlayoutBuildMode.Reset)
{
context.Playout.Seed = new Random().Next();
context.Playout.Items.Clear();
foreach (PlayoutAnchor prevAnchor in Optional(playout.Anchor))
{
context.GuideGroup = prevAnchor.NextGuideGroup;
start = new DateTimeOffset(prevAnchor.NextStart.ToLocalTime(), start.Offset);
context.CurrentTime = start;
context.InstructionIndex = prevAnchor.NextInstructionIndex;
}
}
else
{
// reset (remove items and "currently active" history)
// for testing
// start = start.AddHours(-2);
// erase items, not history
playout.Items.Clear();
// 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 => h.When > start.UtcDateTime || h.When <= start.UtcDateTime && h.Finish >= start.UtcDateTime));
foreach (PlayoutHistory history in toRemove)
{
playout.PlayoutHistory.Remove(history);
}
}
// logger.LogDebug(
// "Saved yaml context from {Start} to {Finish}, instruction {Instruction}",
// context.CurrentTime,
// finish,
// context.InstructionIndex);
// apply all history
var applyHistoryHandler = new YamlPlayoutApplyHistoryHandler(enumeratorCache);
foreach (YamlPlayoutContentItem contentItem in playoutDefinition.Content)
{
await applyHistoryHandler.Handle(context, contentItem, logger, cancellationToken);
}
if (mode is PlayoutBuildMode.Reset)
{
// handle all on-reset instructions
foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
{
@ -110,6 +159,28 @@ public class YamlPlayoutBuilder( @@ -110,6 +159,28 @@ public class YamlPlayoutBuilder(
}
}
CleanUpHistory(playout, start);
DateTime maxTime = context.CurrentTime.UtcDateTime;
if (playout.Items.Count > 0)
{
maxTime = playout.Items.Max(i => i.Finish);
}
var anchor = new PlayoutAnchor
{
NextStart = maxTime,
NextInstructionIndex = context.InstructionIndex,
NextGuideGroup = context.GuideGroup
};
// logger.LogDebug(
// "Saving yaml context at {Start}, instruction {Instruction}",
// maxTime,
// context.InstructionIndex);
playout.Anchor = anchor;
return playout;
}
@ -169,7 +240,8 @@ public class YamlPlayoutBuilder( @@ -169,7 +240,8 @@ public class YamlPlayoutBuilder(
YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(),
YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(),
YamlPlayoutShuffleSequenceInstruction => new YamlPlayoutShuffleSequenceHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(enumeratorCache),
YamlPlayoutSkipToItemInstruction => new YamlPlayoutSkipToItemHandler(enumeratorCache),
// content handlers
@ -237,4 +309,34 @@ public class YamlPlayoutBuilder( @@ -237,4 +309,34 @@ public class YamlPlayoutBuilder(
throw;
}
}
private static void CleanUpHistory(Playout playout, DateTimeOffset start)
{
var groups = new Dictionary<string, List<PlayoutHistory>>();
foreach (PlayoutHistory history in playout.PlayoutHistory)
{
if (!groups.TryGetValue(history.Key, out List<PlayoutHistory> group))
{
group = [];
groups[history.Key] = group;
}
group.Add(history);
}
foreach ((string _, List<PlayoutHistory> group) in groups)
{
//logger.LogDebug("History key {Key} has {Count} items in group", key, group.Count);
IEnumerable<PlayoutHistory> toDelete = group
.Filter(h => h.When < start.UtcDateTime)
.OrderByDescending(h => h.When)
.Tail();
foreach (PlayoutHistory delete in toDelete)
{
playout.PlayoutHistory.Remove(delete);
}
}
}
}

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

@ -5,16 +5,26 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling; @@ -5,16 +5,26 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definition)
{
private readonly System.Collections.Generic.HashSet<int> _visitedInstructions = [];
private int _instructionIndex;
public Playout Playout { get; } = playout;
public YamlPlayoutDefinition Definition { get; } = definition;
public DateTimeOffset CurrentTime { get; set; }
public int InstructionIndex { get; set; }
public int InstructionIndex
{
get => _instructionIndex;
set
{
_instructionIndex = value;
_visitedInstructions.Add(value);
}
}
public int GuideGroup { get; set; }
public bool VisitedAll => _visitedInstructions.Count >= Definition.Playout.Count;
// only used for initial state (skip items)
public Dictionary<string, int> ContentIndex { get; } = [];
public int GuideGroup { get; set; }
}

5866
ErsatzTV.Infrastructure.MySql/Migrations/20240729175905_Add_PlayoutHistory_Finish.Designer.cs generated

File diff suppressed because it is too large Load Diff

41
ErsatzTV.Infrastructure.MySql/Migrations/20240729175905_Add_PlayoutHistory_Finish.cs

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistory_Finish : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "Finish",
table: "PlayoutHistory",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "NextInstructionIndex",
table: "PlayoutAnchor",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Finish",
table: "PlayoutHistory");
migrationBuilder.DropColumn(
name: "NextInstructionIndex",
table: "PlayoutAnchor");
}
}
}

6
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -2452,6 +2452,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2452,6 +2452,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("Details")
.HasColumnType("longtext");
b.Property<DateTime>("Finish")
.HasColumnType("datetime(6)");
b.Property<int>("Index")
.HasColumnType("int");
@ -4312,6 +4315,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4312,6 +4315,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b1.Property<int>("NextGuideGroup")
.HasColumnType("int");
b1.Property<int>("NextInstructionIndex")
.HasColumnType("int");
b1.Property<DateTime>("NextStart")
.HasColumnType("datetime(6)");

5705
ErsatzTV.Infrastructure.Sqlite/Migrations/20240729175656_Add_PlayoutHistory_Finish.Designer.cs generated

File diff suppressed because it is too large Load Diff

41
ErsatzTV.Infrastructure.Sqlite/Migrations/20240729175656_Add_PlayoutHistory_Finish.cs

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistory_Finish : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "Finish",
table: "PlayoutHistory",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "NextInstructionIndex",
table: "PlayoutAnchor",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Finish",
table: "PlayoutHistory");
migrationBuilder.DropColumn(
name: "NextInstructionIndex",
table: "PlayoutAnchor");
}
}
}

6
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -2327,6 +2327,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2327,6 +2327,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("Details")
.HasColumnType("TEXT");
b.Property<DateTime>("Finish")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
@ -4151,6 +4154,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4151,6 +4154,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b1.Property<int>("NextGuideGroup")
.HasColumnType("INTEGER");
b1.Property<int>("NextInstructionIndex")
.HasColumnType("INTEGER");
b1.Property<DateTime>("NextStart")
.HasColumnType("TEXT");

2
ErsatzTV/Pages/BlockPlayoutEditor.razor

@ -142,7 +142,7 @@ @@ -142,7 +142,7 @@
private async Task EraseItems(bool eraseHistory)
{
IRequest request = eraseHistory ? new EraseBlockPlayoutHistory(Id) : new EraseBlockPlayoutItems(Id);
IRequest request = eraseHistory ? new ErasePlayoutHistory(Id) : new ErasePlayoutItems(Id);
await Mediator.Send(request, _cts.Token);
string message = eraseHistory ? "Erased playout items and history" : "Erased playout items";

2
ErsatzTV/Pages/PlayoutEditor.razor

@ -50,6 +50,8 @@ @@ -50,6 +50,8 @@
case PlayoutKind.Yaml:
<MudTextField Label="YAML File" @bind-Value="_model.YamlFile" For="@(() => _model.YamlFile)"/>
break;
case PlayoutKind.Block:
break;
default:
<MudSelect Class="mt-3"
T="ProgramScheduleViewModel"

23
ErsatzTV/Pages/Playouts.razor

@ -133,10 +133,10 @@ @@ -133,10 +133,10 @@
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml)
{
<MudTooltip Text="Edit YAML File">
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => EditYamlFile(context))">
Href="@($"playouts/yaml/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
@ -286,25 +286,6 @@ @@ -286,25 +286,6 @@
}
}
private async Task EditYamlFile(PlayoutNameViewModel playout)
{
var parameters = new DialogParameters { { "YamlFile", $"{playout.TemplateFile}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge };
IDialogReference dialog = await Dialog.ShowAsync<EditYamlFileDialog>("Edit YAML File", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await Mediator.Send(new UpdateYamlPlayout(playout.PlayoutId, result.Data as string ?? playout.TemplateFile), _cts.Token);
if (_table != null)
{
await _table.ReloadServerData();
}
_selectedPlayoutId = null;
}
}
private async Task DeletePlayout(PlayoutNameViewModel playout)
{
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } };

122
ErsatzTV/Pages/YamlPlayoutEditor.razor

@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
@page "/playouts/yaml/{Id:int}"
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.Playouts
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IEntityLocker EntityLocker;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit YAML Playout - @_channelName</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">YAML File</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => EditYamlFile())" Class="mt-4">
Edit YAML File
</MudButton>
</MudCardContent>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Playout Items and History</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<!-- reset will erase all items -->
<!--
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" Class="mt-4">
Erase Items
</MudButton>
</div>
-->
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" Class="mt-4">
Erase Items and History
</MudButton>
</div>
</MudCardContent>
</MudCard>
</div>
</MudItem>
</MudGrid>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private PlayoutNameViewModel _playout;
[Parameter]
public int Id { get; set; }
private string _channelName;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("/playouts");
return;
}
foreach (string name in maybeName)
{
_channelName = name;
}
Option<PlayoutNameViewModel> maybePlayout = await Mediator.Send(new GetPlayoutById(Id), _cts.Token);
foreach (PlayoutNameViewModel playout in maybePlayout)
{
_playout = playout;
}
}
private async Task EraseItems(bool eraseHistory)
{
IRequest request = eraseHistory ? new ErasePlayoutHistory(Id) : new ErasePlayoutItems(Id);
await Mediator.Send(request, _cts.Token);
string message = eraseHistory ? "Erased playout items and history" : "Erased playout items";
Snackbar.Add(message, Severity.Info);
}
private async Task EditYamlFile()
{
if (_playout is null)
{
return;
}
var parameters = new DialogParameters { { "YamlFile", $"{_playout.TemplateFile}" } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge };
IDialogReference dialog = await Dialog.ShowAsync<EditYamlFileDialog>("Edit YAML File", parameters, options);
DialogResult result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await Mediator.Send(new UpdateYamlPlayout(_playout.PlayoutId, result.Data as string ?? _playout.TemplateFile), _cts.Token);
}
}
}
Loading…
Cancel
Save