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 12 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
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
TemplateFile = externalJsonFile, TemplateFile = externalJsonFile,
ProgramSchedulePlayoutType = playoutType ProgramSchedulePlayoutType = playoutType,
Seed = new Random().Next()
}); });
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(

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

@ -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 @@
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 @@
namespace ErsatzTV.Application.Scheduling;
public record EraseBlockPlayoutHistory(int PlayoutId) : IRequest;

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

@ -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 @@
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;
namespace ErsatzTV.Application.Scheduling; namespace ErsatzTV.Application.Scheduling;
public class EraseBlockPlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFactory) public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<EraseBlockPlayoutHistory> : 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); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts 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); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)

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

@ -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;
namespace ErsatzTV.Application.Scheduling; namespace ErsatzTV.Application.Scheduling;
public class EraseBlockPlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFactory) public class ErasePlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<EraseBlockPlayoutItems> : 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); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Items) .Include(p => p.Items)
.Include(p => p.PlayoutHistory) .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); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)

1
ErsatzTV.Core/Domain/PlayoutAnchor.cs

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

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

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

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

@ -1,9 +1,10 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace ErsatzTV.Core.Scheduling.BlockScheduling; namespace ErsatzTV.Core.Scheduling;
internal static class HistoryDetails internal static class HistoryDetails
{ {
@ -28,6 +29,17 @@ internal static class HistoryDetails
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings); 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) public static string ForDefaultFiller(Deco deco)
{ {
dynamic key = new dynamic key = new

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

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

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

@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -57,6 +58,19 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
context.Playout.Items.Add(playoutItem); 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; context.CurrentTime += itemDuration;
enumerator.MoveNext(); enumerator.MoveNext();
} }

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

@ -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 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
@ -45,6 +46,42 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
return maybeEnumerator; 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) protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{ {
if (mediaItem is Image image) if (mediaItem is Image image)

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

@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -57,6 +58,19 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
context.Playout.Items.Add(playoutItem); 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; context.CurrentTime += itemDuration;
enumerator.MoveNext(); enumerator.MoveNext();
} }

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

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

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

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

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

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

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

@ -20,7 +20,7 @@ public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler
return Task.FromResult(false); 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"); logger.LogWarning("Repeat encountered without adding any playout items; aborting");
throw new InvalidOperationException("YAML playout loop detected"); throw new InvalidOperationException("YAML playout loop detected");

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

@ -1,13 +1,14 @@
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler public class YamlPlayoutSkipItemsHandler(EnumeratorCache enumeratorCache) : IYamlPlayoutHandler
{ {
public bool Reset => true; public bool Reset => true;
public Task<bool> Handle( public async Task<bool> Handle(
YamlPlayoutContext context, YamlPlayoutContext context,
YamlPlayoutInstruction instruction, YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger, ILogger<YamlPlayoutBuilder> logger,
@ -15,19 +16,28 @@ public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler
{ {
if (instruction is not YamlPlayoutSkipItemsInstruction skipItems) 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 true;
return Task.FromResult(true);
} }
} }

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

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

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

@ -1,7 +1,12 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentItem public class YamlPlayoutContentItem
{ {
public string Key { get; set; } public string Key { get; set; }
public string Order { 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 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
@ -29,32 +30,80 @@ public class YamlPlayoutBuilder(
YamlPlayoutDefinition playoutDefinition = await LoadYamlDefinition(playout, cancellationToken); YamlPlayoutDefinition playoutDefinition = await LoadYamlDefinition(playout, cancellationToken);
DateTimeOffset start = DateTimeOffset.Now; DateTimeOffset start = DateTimeOffset.Now;
int daysToBuild = await GetDaysToBuild(); int daysToBuild = await GetDaysToBuild();
DateTimeOffset finish = start.AddDays(daysToBuild); 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(); Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers = new();
var enumeratorCache = new EnumeratorCache(mediaCollectionRepository); var enumeratorCache = new EnumeratorCache(mediaCollectionRepository);
var context = new YamlPlayoutContext(playout, playoutDefinition) var context = new YamlPlayoutContext(playout, playoutDefinition)
{ {
CurrentTime = start, CurrentTime = start,
GuideGroup = 1, GuideGroup = 1
InstructionIndex = 0
// no need to init default value and throw off visited count
// InstructionIndex = 0
}; };
// ReSharper disable once ConditionIsAlwaysTrueOrFalse // logger.LogDebug(
if (mode is PlayoutBuildMode.Reset) // "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(); foreach (PlayoutAnchor prevAnchor in Optional(playout.Anchor))
context.Playout.Items.Clear(); {
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 // handle all on-reset instructions
foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset) foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
{ {
@ -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; return playout;
} }
@ -169,7 +240,8 @@ public class YamlPlayoutBuilder(
YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(), YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(),
YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(), YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(),
YamlPlayoutShuffleSequenceInstruction => new YamlPlayoutShuffleSequenceHandler(), YamlPlayoutShuffleSequenceInstruction => new YamlPlayoutShuffleSequenceHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(enumeratorCache),
YamlPlayoutSkipToItemInstruction => new YamlPlayoutSkipToItemHandler(enumeratorCache), YamlPlayoutSkipToItemInstruction => new YamlPlayoutSkipToItemHandler(enumeratorCache),
// content handlers // content handlers
@ -237,4 +309,34 @@ public class YamlPlayoutBuilder(
throw; 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;
public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definition) public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definition)
{ {
private readonly System.Collections.Generic.HashSet<int> _visitedInstructions = [];
private int _instructionIndex;
public Playout Playout { get; } = playout; public Playout Playout { get; } = playout;
public YamlPlayoutDefinition Definition { get; } = definition; public YamlPlayoutDefinition Definition { get; } = definition;
public DateTimeOffset CurrentTime { get; set; } 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 int GuideGroup { get; set; }
public Dictionary<string, int> ContentIndex { get; } = [];
} }

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 @@
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
b.Property<string>("Details") b.Property<string>("Details")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<DateTime>("Finish")
.HasColumnType("datetime(6)");
b.Property<int>("Index") b.Property<int>("Index")
.HasColumnType("int"); .HasColumnType("int");
@ -4312,6 +4315,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b1.Property<int>("NextGuideGroup") b1.Property<int>("NextGuideGroup")
.HasColumnType("int"); .HasColumnType("int");
b1.Property<int>("NextInstructionIndex")
.HasColumnType("int");
b1.Property<DateTime>("NextStart") b1.Property<DateTime>("NextStart")
.HasColumnType("datetime(6)"); .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 @@
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
b.Property<string>("Details") b.Property<string>("Details")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<DateTime>("Finish")
.HasColumnType("TEXT");
b.Property<int>("Index") b.Property<int>("Index")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -4151,6 +4154,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b1.Property<int>("NextGuideGroup") b1.Property<int>("NextGuideGroup")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b1.Property<int>("NextInstructionIndex")
.HasColumnType("INTEGER");
b1.Property<DateTime>("NextStart") b1.Property<DateTime>("NextStart")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

2
ErsatzTV/Pages/BlockPlayoutEditor.razor

@ -142,7 +142,7 @@
private async Task EraseItems(bool eraseHistory) 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); await Mediator.Send(request, _cts.Token);
string message = eraseHistory ? "Erased playout items and history" : "Erased playout items"; string message = eraseHistory ? "Erased playout items and history" : "Erased playout items";

2
ErsatzTV/Pages/PlayoutEditor.razor

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

23
ErsatzTV/Pages/Playouts.razor

@ -133,10 +133,10 @@
} }
else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml) else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml)
{ {
<MudTooltip Text="Edit YAML File"> <MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => EditYamlFile(context))"> Href="@($"playouts/yaml/{context.PlayoutId}")">
</MudIconButton> </MudIconButton>
</MudTooltip> </MudTooltip>
<MudTooltip Text="Reset Playout"> <MudTooltip Text="Reset Playout">
@ -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) private async Task DeletePlayout(PlayoutNameViewModel playout)
{ {
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } }; var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } };

122
ErsatzTV/Pages/YamlPlayoutEditor.razor

@ -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