Browse Source

refactor yaml playout builder (#1820)

* update changelog

* refactor some handlers

* refactor skip items instruction

* more refactoring
pull/1821/head
Jason Dove 12 months ago committed by GitHub
parent
commit
e4e4f68eb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 85
      ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs
  3. 15
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs
  4. 65
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  5. 70
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  6. 88
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  7. 24
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutNewEpgGroupHandler.cs
  8. 68
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs
  9. 33
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs
  10. 33
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs
  11. 47
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs
  12. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentGuid.cs
  13. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs
  14. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSearchItem.cs
  15. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentShowItem.cs
  16. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutCountInstruction.cs
  17. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs
  18. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDurationInstruction.cs
  19. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs
  20. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutNewEpgGroupInstruction.cs
  21. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutPadToNextInstruction.cs
  22. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutRepeatInstruction.cs
  23. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSkipItemsInstruction.cs
  24. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWaitUntilInstruction.cs
  25. 277
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  26. 20
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs
  27. 8
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInitialState.cs
  28. 31
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutScheduler.cs
  29. 53
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerCount.cs
  30. 47
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerPadToNext.cs

2
CHANGELOG.md

@ -42,6 +42,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -42,6 +42,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix infinite loop caused by impossible schedule (all collection items longer than schedule item duration)
- Fix selecting audio and subtitle streams with two-letter language codes
- Fix adding pad filler to content that is less than one minute in duration
- Generate unique identifier for virtual HDHomeRun tuner by @raknam
- This allows a single Plex server to connect to multiple ETV instances
### Changed
- Remove some unnecessary API calls related to media server scanning and paging

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

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepository)
{
private readonly Dictionary<string, IMediaCollectionEnumerator> _enumerators = new();
public System.Collections.Generic.HashSet<string> MissingContentKeys { get; } = [];
public async Task<Option<IMediaCollectionEnumerator>> GetCachedEnumeratorForContent(
YamlPlayoutContext context,
string contentKey,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(contentKey))
{
return Option<IMediaCollectionEnumerator>.None;
}
if (!_enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator))
{
Option<IMediaCollectionEnumerator> maybeEnumerator =
await GetEnumeratorForContent(context, contentKey, cancellationToken);
if (maybeEnumerator.IsNone)
{
return Option<IMediaCollectionEnumerator>.None;
}
foreach (IMediaCollectionEnumerator e in maybeEnumerator)
{
enumerator = e;
_enumerators.Add(contentKey, enumerator);
}
}
return Some(enumerator);
}
private async Task<Option<IMediaCollectionEnumerator>> GetEnumeratorForContent(
YamlPlayoutContext context,
string contentKey,
CancellationToken cancellationToken)
{
int index = context.Definition.Content.FindIndex(c => c.Key == contentKey);
if (index < 0)
{
return Option<IMediaCollectionEnumerator>.None;
}
List<MediaItem> items = [];
YamlPlayoutContentItem content = context.Definition.Content[index];
switch (content)
{
case YamlPlayoutContentSearchItem search:
items = await mediaCollectionRepository.GetSmartCollectionItems(search.Query);
break;
case YamlPlayoutContentShowItem show:
items = await mediaCollectionRepository.GetShowItemsByShowGuids(
show.Guids.Map(g => $"{g.Source}://{g.Value}").ToList());
break;
}
// start at the appropriate place in the enumerator
context.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex);
var state = new CollectionEnumeratorState { Seed = context.Playout.Seed + index, Index = enumeratorIndex };
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();
return new ShuffledMediaCollectionEnumerator(groupedMediaItems, state, cancellationToken);
}
return Option<IMediaCollectionEnumerator>.None;
}
}

15
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public interface IYamlPlayoutHandler
{
bool Reset { get; }
Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken);
}

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

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) : IYamlPlayoutHandler
{
public bool Reset => false;
public abstract Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken);
protected async Task<Option<IMediaCollectionEnumerator>> GetContentEnumerator(
YamlPlayoutContext context,
string contentKey,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
Option<IMediaCollectionEnumerator> maybeEnumerator = await enumeratorCache.GetCachedEnumeratorForContent(
context,
contentKey,
cancellationToken);
if (maybeEnumerator.IsNone)
{
if (!enumeratorCache.MissingContentKeys.Contains(contentKey))
{
logger.LogWarning("Unable to locate content with key {Key}", contentKey);
enumeratorCache.MissingContentKeys.Add(contentKey);
}
}
return maybeEnumerator;
}
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
if (mediaItem is Image image)
{
return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds);
}
MediaVersion version = mediaItem.GetHeadVersion();
return version.Duration;
}
protected static FillerKind GetFillerKind(YamlPlayoutInstruction instruction)
{
if (string.IsNullOrWhiteSpace(instruction.FillerKind))
{
return FillerKind.None;
}
return Enum.TryParse(instruction.FillerKind, ignoreCase: true, out FillerKind result)
? result
: FillerKind.None;
}
}

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

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlayoutContentHandler(enumeratorCache)
{
public override async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutCountInstruction count)
{
return false;
}
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetContentEnumerator(
context,
instruction.Content,
logger,
cancellationToken);
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
for (var i = 0; i < count.Count; i++)
{
foreach (MediaItem mediaItem in enumerator.Current)
{
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
// create a playout item
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = context.CurrentTime.UtcDateTime,
Finish = context.CurrentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
FillerKind = GetFillerKind(count),
//CustomTitle = scheduleItem.CustomTitle,
//WatermarkId = scheduleItem.WatermarkId,
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
//SubtitleMode = scheduleItem.SubtitleMode
GuideGroup = context.GuideGroup
//GuideStart = effectiveBlock.Start.UtcDateTime,
//GuideFinish = blockFinish.UtcDateTime,
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
//CollectionEtag = collectionEtags[collectionKey]
};
context.Playout.Items.Add(playoutItem);
context.CurrentTime += itemDuration;
enumerator.MoveNext();
}
}
return true;
}
return false;
}
}

88
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerDuration.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs

@ -1,53 +1,73 @@ @@ -1,53 +1,73 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
using TimeSpanParserUtil;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler
public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlPlayoutContentHandler(enumeratorCache)
{
public static DateTimeOffset Schedule(
Playout playout,
DateTimeOffset currentTime,
int guideGroup,
YamlPlayoutDurationInstruction duration,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator)
public override async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutDurationInstruction duration)
{
return false;
}
// TODO: move to up-front validation somewhere
if (!TimeSpanParser.TryParse(duration.Duration, out TimeSpan timeSpan))
{
return currentTime;
return false;
}
DateTimeOffset targetTime = context.CurrentTime.Add(timeSpan);
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetContentEnumerator(
context,
instruction.Content,
logger,
cancellationToken);
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetContentEnumerator(
context,
duration.Fallback,
logger,
cancellationToken);
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
context.CurrentTime = Schedule(
context,
targetTime,
duration.DiscardAttempts,
duration.Trim,
GetFillerKind(duration),
enumerator,
fallbackEnumerator);
return true;
}
DateTimeOffset targetTime = currentTime.Add(timeSpan);
return Schedule(
playout,
currentTime,
targetTime,
duration.DiscardAttempts,
duration.Trim,
GetFillerKind(duration),
guideGroup,
enumerator,
fallbackEnumerator);
return false;
}
protected static DateTimeOffset Schedule(
Playout playout,
DateTimeOffset currentTime,
YamlPlayoutContext context,
DateTimeOffset targetTime,
int discardAttempts,
bool trim,
FillerKind fillerKind,
int guideGroup,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator)
{
bool done = false;
TimeSpan remainingToFill = targetTime - currentTime;
TimeSpan remainingToFill = targetTime - context.CurrentTime;
while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero)
{
foreach (MediaItem mediaItem in enumerator.Current)
@ -57,11 +77,11 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler @@ -57,11 +77,11 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = currentTime.UtcDateTime,
Finish = currentTime.UtcDateTime + itemDuration,
Start = context.CurrentTime.UtcDateTime,
Finish = context.CurrentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = guideGroup,
GuideGroup = context.GuideGroup,
FillerKind = fillerKind
//DisableWatermarks = !allowWatermarks
};
@ -69,9 +89,9 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler @@ -69,9 +89,9 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler
if (remainingToFill - itemDuration >= TimeSpan.Zero)
{
remainingToFill -= itemDuration;
currentTime += itemDuration;
context.CurrentTime += itemDuration;
playout.Items.Add(playoutItem);
context.Playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (discardAttempts > 0)
@ -84,12 +104,12 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler @@ -84,12 +104,12 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler
{
// trim item to exactly fit
remainingToFill = TimeSpan.Zero;
currentTime = targetTime;
context.CurrentTime = targetTime;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start;
playout.Items.Add(playoutItem);
context.Playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (fallbackEnumerator.IsSome)
@ -106,7 +126,7 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler @@ -106,7 +126,7 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.FillerKind = FillerKind.Fallback;
playout.Items.Add(playoutItem);
context.Playout.Items.Add(playoutItem);
fallback.MoveNext();
}
}

24
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutNewEpgGroupHandler.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutNewEpgGroupHandler : IYamlPlayoutHandler
{
public bool Reset => false;
public Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutNewEpgGroupInstruction)
{
return Task.FromResult(false);
}
context.GuideGroup *= -1;
return Task.FromResult(true);
}
}

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

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : YamlPlayoutDurationHandler(enumeratorCache)
{
public override async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutPadToNextInstruction padToNext)
{
return false;
}
int currentMinute = context.CurrentTime.Minute;
int targetMinute = (currentMinute + padToNext.PadToNext - 1) / padToNext.PadToNext * padToNext.PadToNext;
DateTimeOffset almostTargetTime =
context.CurrentTime - TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute);
var targetTime = new DateTimeOffset(
almostTargetTime.Year,
almostTargetTime.Month,
almostTargetTime.Day,
almostTargetTime.Hour,
almostTargetTime.Minute,
0,
almostTargetTime.Offset);
// ensure filler works for content less than one minute
if (targetTime <= context.CurrentTime)
targetTime = targetTime.AddMinutes(padToNext.PadToNext);
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetContentEnumerator(
context,
instruction.Content,
logger,
cancellationToken);
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetContentEnumerator(
context,
padToNext.Fallback,
logger,
cancellationToken);
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
context.CurrentTime = Schedule(
context,
targetTime,
padToNext.DiscardAttempts,
padToNext.Trim,
GetFillerKind(padToNext),
enumerator,
fallbackEnumerator);
return true;
}
return false;
}
}

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

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler
{
private int _itemsSinceLastRepeat;
public bool Reset => false;
public Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutRepeatInstruction)
{
return Task.FromResult(false);
}
if (_itemsSinceLastRepeat == context.Playout.Items.Count)
{
logger.LogWarning("Repeat encountered without adding any playout items; aborting");
return Task.FromResult(false);
}
_itemsSinceLastRepeat = context.Playout.Items.Count;
context.InstructionIndex = 0;
return Task.FromResult(true);
}
}

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

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler
{
public bool Reset => true;
public Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutSkipItemsInstruction skipItems)
{
return Task.FromResult(false);
}
if (context.ContentIndex.TryGetValue(skipItems.Content, out int value))
{
value += skipItems.SkipItems;
}
else
{
value = skipItems.SkipItems;
}
context.ContentIndex[skipItems.Content] = value;
return Task.FromResult(true);
}
}

47
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutWaitUntilHandler : IYamlPlayoutHandler
{
public bool Reset => true;
public Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutWaitUntilInstruction waitUntil)
{
return Task.FromResult(false);
}
DateTimeOffset currentTime = context.CurrentTime;
if (TimeOnly.TryParse(waitUntil.WaitUntil, out TimeOnly result))
{
var dayOnly = DateOnly.FromDateTime(currentTime.LocalDateTime);
var timeOnly = TimeOnly.FromDateTime(currentTime.LocalDateTime);
if (timeOnly > result)
{
if (waitUntil.Tomorrow)
{
// this is wrong when offset changes
dayOnly = dayOnly.AddDays(1);
currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset);
}
}
else
{
// this is wrong when offset changes
currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset);
}
}
context.CurrentTime = currentTime;
return Task.FromResult(true);
}
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentGuid.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentGuid.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentGuid
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentItem.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentItem
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentSearchItem.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSearchItem.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentSearchItem : YamlPlayoutContentItem
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentShowItem.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentShowItem.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentShowItem : YamlPlayoutContentItem
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutCountInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutCountInstruction.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutCountInstruction : YamlPlayoutInstruction
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDefinition.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutDefinition
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDurationInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDurationInstruction.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutDurationInstruction : YamlPlayoutInstruction
{

4
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutInstruction
{
public virtual bool ChangesIndex => false;
public string Content { get; set; }
[YamlMember(Alias = "filler_kind", ApplyNamingConventions = false)]

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutNewEpgGroupInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutNewEpgGroupInstruction.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutNewEpgGroupInstruction : YamlPlayoutInstruction
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutPadToNextInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutPadToNextInstruction.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutPadToNextInstruction : YamlPlayoutInstruction
{

4
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutRepeatInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutRepeatInstruction.cs

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutRepeatInstruction : YamlPlayoutInstruction
{
public override bool ChangesIndex => true;
public bool Repeat { get; set; }
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSkipItemsInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSkipItemsInstruction.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutSkipItemsInstruction : YamlPlayoutInstruction
{

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutWaitUntilInstruction.cs → ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWaitUntilInstruction.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutWaitUntilInstruction : YamlPlayoutInstruction
{

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

@ -2,6 +2,8 @@ using ErsatzTV.Core.Domain; @@ -2,6 +2,8 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
@ -35,181 +37,74 @@ public class YamlPlayoutBuilder( @@ -35,181 +37,74 @@ public class YamlPlayoutBuilder(
return playout;
}
YamlPlayoutInitialState initialState = HandleResetActions(playout, playoutDefinition, start);
DateTimeOffset currentTime = initialState.CurrentTime;
// load content and content enumerators on demand
Dictionary<string, IMediaCollectionEnumerator> enumerators = new();
System.Collections.Generic.HashSet<string> missingContentKeys = [];
Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers = new();
var enumeratorCache = new EnumeratorCache(mediaCollectionRepository);
int itemsAfterRepeat = playout.Items.Count;
int guideGroup = 1;
var index = 0;
while (currentTime < finish)
var context = new YamlPlayoutContext(playout, playoutDefinition)
{
if (index >= playoutDefinition.Playout.Count)
{
logger.LogInformation("Reached the end of the YAML playout definition; stopping");
break;
}
CurrentTime = start,
GuideGroup = 1,
InstructionIndex = 0
};
YamlPlayoutInstruction playoutItem = playoutDefinition.Playout[index];
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (mode is PlayoutBuildMode.Reset)
{
context.Playout.Seed = new Random().Next();
context.Playout.Items.Clear();
// handle instructions that don't reference content
switch (playoutItem)
// handle all on-reset instructions
foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
{
case YamlPlayoutWaitUntilInstruction waitUntil:
currentTime = HandleWaitUntil(currentTime, waitUntil);
index++;
continue;
case YamlPlayoutRepeatInstruction:
// repeat resets index into YAML playout
index = 0;
if (playout.Items.Count == itemsAfterRepeat)
{
logger.LogWarning("Repeat encountered without adding any playout items; aborting");
break;
}
itemsAfterRepeat = playout.Items.Count;
continue;
case YamlPlayoutNewEpgGroupInstruction:
guideGroup *= -1;
index++;
continue;
}
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetCachedEnumeratorForContent(
initialState,
playout,
playoutDefinition,
enumerators,
playoutItem.Content,
cancellationToken);
Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(
handlers,
enumeratorCache,
instruction);
if (maybeEnumerator.IsNone)
{
if (!missingContentKeys.Contains(playoutItem.Content))
foreach (IYamlPlayoutHandler handler in maybeHandler)
{
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content);
missingContentKeys.Add(playoutItem.Content);
}
}
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
switch (playoutItem)
{
case YamlPlayoutCountInstruction count:
currentTime = YamlPlayoutSchedulerCount.Schedule(playout, currentTime, guideGroup, count, enumerator);
break;
case YamlPlayoutDurationInstruction duration:
Option<IMediaCollectionEnumerator> durationFallbackEnumerator = await GetCachedEnumeratorForContent(
initialState,
playout,
playoutDefinition,
enumerators,
duration.Fallback,
cancellationToken);
currentTime = YamlPlayoutSchedulerDuration.Schedule(
playout,
currentTime,
guideGroup,
duration,
enumerator,
durationFallbackEnumerator);
break;
case YamlPlayoutPadToNextInstruction padToNext:
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetCachedEnumeratorForContent(
initialState,
playout,
playoutDefinition,
enumerators,
padToNext.Fallback,
cancellationToken);
currentTime = YamlPlayoutSchedulerPadToNext.Schedule(
playout,
currentTime,
guideGroup,
padToNext,
enumerator,
fallbackEnumerator);
break;
}
}
index++;
}
return playout;
}
private YamlPlayoutInitialState HandleResetActions(
Playout playout,
YamlPlayoutDefinition playoutDefinition,
DateTimeOffset currentTime)
{
var result = new YamlPlayoutInitialState { CurrentTime = currentTime };
// these are only for reset
playout.Seed = new Random().Next();
playout.Items.Clear();
foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
{
switch (instruction)
{
case YamlPlayoutWaitUntilInstruction waitUntil:
result.CurrentTime = HandleWaitUntil(result.CurrentTime, waitUntil);
break;
case YamlPlayoutSkipItemsInstruction skipItems:
if (result.ContentIndex.TryGetValue(skipItems.Content, out int value))
if (!handler.Reset)
{
value += skipItems.SkipItems;
logger.LogInformation(
"Skipping unsupported reset instruction {Instruction}",
instruction.GetType().Name);
}
else
{
value = skipItems.SkipItems;
await handler.Handle(context, instruction, logger, cancellationToken);
}
result.ContentIndex[skipItems.Content] = value;
break;
default:
logger.LogInformation(
"Skipping unsupported reset instruction {Instruction}",
instruction.GetType().Name);
break;
}
}
}
return result;
}
private static DateTimeOffset HandleWaitUntil(DateTimeOffset currentTime, YamlPlayoutWaitUntilInstruction waitUntil)
{
if (TimeOnly.TryParse(waitUntil.WaitUntil, out TimeOnly result))
// handle all playout instructions
while (context.CurrentTime < finish)
{
var dayOnly = DateOnly.FromDateTime(currentTime.LocalDateTime);
var timeOnly = TimeOnly.FromDateTime(currentTime.LocalDateTime);
if (context.InstructionIndex >= playoutDefinition.Playout.Count)
{
logger.LogInformation("Reached the end of the YAML playout definition; stopping");
break;
}
YamlPlayoutInstruction instruction = playoutDefinition.Playout[context.InstructionIndex];
Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(handlers, enumeratorCache, instruction);
if (timeOnly > result)
foreach (IYamlPlayoutHandler handler in maybeHandler)
{
if (waitUntil.Tomorrow)
if (!await handler.Handle(context, instruction, logger, cancellationToken))
{
// this is wrong when offset changes
dayOnly = dayOnly.AddDays(1);
currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset);
logger.LogInformation("YAML playout instruction handler failed");
}
}
else
if (!instruction.ChangesIndex)
{
// this is wrong when offset changes
currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset);
context.InstructionIndex++;
}
}
return currentTime;
return playout;
}
private async Task<int> GetDaysToBuild() =>
@ -217,81 +112,37 @@ public class YamlPlayoutBuilder( @@ -217,81 +112,37 @@ public class YamlPlayoutBuilder(
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.IfNoneAsync(2);
private async Task<Option<IMediaCollectionEnumerator>> GetCachedEnumeratorForContent(
YamlPlayoutInitialState initialState,
Playout playout,
YamlPlayoutDefinition playoutDefinition,
Dictionary<string, IMediaCollectionEnumerator> enumerators,
string contentKey,
CancellationToken cancellationToken)
private static Option<IYamlPlayoutHandler> GetHandlerForInstruction(
Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers,
EnumeratorCache enumeratorCache,
YamlPlayoutInstruction instruction)
{
if (string.IsNullOrWhiteSpace(contentKey))
{
return Option<IMediaCollectionEnumerator>.None;
}
if (!enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator))
if (handlers.TryGetValue(instruction, out IYamlPlayoutHandler handler))
{
Option<IMediaCollectionEnumerator> maybeEnumerator =
await GetEnumeratorForContent(initialState, playout, contentKey, playoutDefinition, cancellationToken);
if (maybeEnumerator.IsNone)
{
return Option<IMediaCollectionEnumerator>.None;
}
foreach (IMediaCollectionEnumerator e in maybeEnumerator)
{
enumerator = e;
enumerators.Add(contentKey, enumerator);
}
return Optional(handler);
}
return Some(enumerator);
}
private async Task<Option<IMediaCollectionEnumerator>> GetEnumeratorForContent(
YamlPlayoutInitialState initialState,
Playout playout,
string contentKey,
YamlPlayoutDefinition playoutDefinition,
CancellationToken cancellationToken)
{
int index = playoutDefinition.Content.FindIndex(c => c.Key == contentKey);
if (index < 0)
handler = instruction switch
{
return Option<IMediaCollectionEnumerator>.None;
}
YamlPlayoutRepeatInstruction => new YamlPlayoutRepeatHandler(),
YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(),
YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(),
List<MediaItem> items = [];
YamlPlayoutContentItem content = playoutDefinition.Content[index];
switch (content)
{
case YamlPlayoutContentSearchItem search:
items = await mediaCollectionRepository.GetSmartCollectionItems(search.Query);
break;
case YamlPlayoutContentShowItem show:
items = await mediaCollectionRepository.GetShowItemsByShowGuids(
show.Guids.Map(g => $"{g.Source}://{g.Value}").ToList());
break;
}
// content handlers
YamlPlayoutCountInstruction => new YamlPlayoutCountHandler(enumeratorCache),
YamlPlayoutDurationInstruction => new YamlPlayoutDurationHandler(enumeratorCache),
YamlPlayoutPadToNextInstruction => new YamlPlayoutPadToNextHandler(enumeratorCache),
// start at the appropriate place in the enumerator
initialState.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex);
_ => null
};
var state = new CollectionEnumeratorState { Seed = playout.Seed + index, Index = enumeratorIndex };
switch (Enum.Parse<PlaybackOrder>(content.Order, true))
if (handler != null)
{
case PlaybackOrder.Chronological:
return new ChronologicalMediaCollectionEnumerator(items, state);
case PlaybackOrder.Shuffle:
// TODO: fix this
var groupedMediaItems = items.Map(mi => new GroupedMediaItem(mi, null)).ToList();
return new ShuffledMediaCollectionEnumerator(groupedMediaItems, state, cancellationToken);
handlers.Add(instruction, handler);
}
return Option<IMediaCollectionEnumerator>.None;
return Optional(handler);
}
private static async Task<YamlPlayoutDefinition> LoadYamlDefinition(Playout playout, CancellationToken cancellationToken)

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

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definition)
{
public Playout Playout { get; } = playout;
public YamlPlayoutDefinition Definition { get; } = definition;
public DateTimeOffset CurrentTime { get; set; }
public int InstructionIndex { get; set; }
public int GuideGroup { get; set; }
// only used for initial state (skip items)
public Dictionary<string, int> ContentIndex { get; } = [];
}

8
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInitialState.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class YamlPlayoutInitialState
{
public DateTimeOffset CurrentTime { get; set; }
public Dictionary<string, int> ContentIndex { get; } = [];
}

31
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutScheduler.cs

@ -1,31 +0,0 @@ @@ -1,31 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Extensions;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public abstract class YamlPlayoutScheduler
{
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
if (mediaItem is Image image)
{
return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds);
}
MediaVersion version = mediaItem.GetHeadVersion();
return version.Duration;
}
protected static FillerKind GetFillerKind(YamlPlayoutInstruction instruction)
{
if (string.IsNullOrWhiteSpace(instruction.FillerKind))
{
return FillerKind.None;
}
return Enum.TryParse(instruction.FillerKind, ignoreCase: true, out FillerKind result)
? result
: FillerKind.None;
}
}

53
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerCount.cs

@ -1,53 +0,0 @@ @@ -1,53 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class YamlPlayoutSchedulerCount : YamlPlayoutScheduler
{
public static DateTimeOffset Schedule(
Playout playout,
DateTimeOffset currentTime,
int guideGroup,
YamlPlayoutCountInstruction count,
IMediaCollectionEnumerator enumerator)
{
for (int i = 0; i < count.Count; i++)
{
foreach (MediaItem mediaItem in enumerator.Current)
{
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
// create a playout item
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = currentTime.UtcDateTime,
Finish = currentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
FillerKind = GetFillerKind(count),
//CustomTitle = scheduleItem.CustomTitle,
//WatermarkId = scheduleItem.WatermarkId,
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
//SubtitleMode = scheduleItem.SubtitleMode
GuideGroup = guideGroup
//GuideStart = effectiveBlock.Start.UtcDateTime,
//GuideFinish = blockFinish.UtcDateTime,
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
//CollectionEtag = collectionEtags[collectionKey]
};
playout.Items.Add(playoutItem);
currentTime += itemDuration;
enumerator.MoveNext();
}
}
return currentTime;
}
}

47
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerPadToNext.cs

@ -1,47 +0,0 @@ @@ -1,47 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class YamlPlayoutSchedulerPadToNext : YamlPlayoutSchedulerDuration
{
public static DateTimeOffset Schedule(
Playout playout,
DateTimeOffset currentTime,
int guideGroup,
YamlPlayoutPadToNextInstruction padToNext,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator)
{
int currentMinute = currentTime.Minute;
int targetMinute = (currentMinute + padToNext.PadToNext - 1) / padToNext.PadToNext * padToNext.PadToNext;
DateTimeOffset almostTargetTime =
currentTime - TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute);
var targetTime = new DateTimeOffset(
almostTargetTime.Year,
almostTargetTime.Month,
almostTargetTime.Day,
almostTargetTime.Hour,
almostTargetTime.Minute,
0,
almostTargetTime.Offset);
// ensure filler works for content less than one minute
if (targetTime <= currentTime)
targetTime = targetTime.AddMinutes(padToNext.PadToNext);
return Schedule(
playout,
currentTime,
targetTime,
padToNext.DiscardAttempts,
padToNext.Trim,
GetFillerKind(padToNext),
guideGroup,
enumerator,
fallbackEnumerator);
}
}
Loading…
Cancel
Save