Browse Source

refactor yaml playout builder (#1820)

* update changelog

* refactor some handlers

* refactor skip items instruction

* more refactoring
pull/1821/head
Jason Dove 2 years 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/).
- Fix infinite loop caused by impossible schedule (all collection items longer than schedule item duration) - 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 selecting audio and subtitle streams with two-letter language codes
- Fix adding pad filler to content that is less than one minute in duration - 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 ### Changed
- Remove some unnecessary API calls related to media server scanning and paging - Remove some unnecessary API calls related to media server scanning and paging

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

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

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

@ -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 @@
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 @@
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 @@
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 @@
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 @@
namespace ErsatzTV.Core.Scheduling.YamlScheduling; namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentGuid public class YamlPlayoutContentGuid
{ {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -2,6 +2,8 @@ using ErsatzTV.Core.Domain;
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;
using ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
@ -35,181 +37,74 @@ public class YamlPlayoutBuilder(
return playout; return playout;
} }
YamlPlayoutInitialState initialState = HandleResetActions(playout, playoutDefinition, start);
DateTimeOffset currentTime = initialState.CurrentTime;
// load content and content enumerators on demand // load content and content enumerators on demand
Dictionary<string, IMediaCollectionEnumerator> enumerators = new(); Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers = new();
System.Collections.Generic.HashSet<string> missingContentKeys = []; var enumeratorCache = new EnumeratorCache(mediaCollectionRepository);
int itemsAfterRepeat = playout.Items.Count; var context = new YamlPlayoutContext(playout, playoutDefinition)
int guideGroup = 1;
var index = 0;
while (currentTime < finish)
{ {
if (index >= playoutDefinition.Playout.Count) CurrentTime = start,
{ GuideGroup = 1,
logger.LogInformation("Reached the end of the YAML playout definition; stopping"); InstructionIndex = 0
break; };
}
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 // handle all on-reset instructions
switch (playoutItem) foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
{ {
case YamlPlayoutWaitUntilInstruction waitUntil: Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(
currentTime = HandleWaitUntil(currentTime, waitUntil); handlers,
index++; enumeratorCache,
continue; instruction);
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);
if (maybeEnumerator.IsNone) foreach (IYamlPlayoutHandler handler in maybeHandler)
{
if (!missingContentKeys.Contains(playoutItem.Content))
{ {
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); if (!handler.Reset)
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))
{ {
value += skipItems.SkipItems; logger.LogInformation(
"Skipping unsupported reset instruction {Instruction}",
instruction.GetType().Name);
} }
else 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; // handle all playout instructions
} while (context.CurrentTime < finish)
private static DateTimeOffset HandleWaitUntil(DateTimeOffset currentTime, YamlPlayoutWaitUntilInstruction waitUntil)
{
if (TimeOnly.TryParse(waitUntil.WaitUntil, out TimeOnly result))
{ {
var dayOnly = DateOnly.FromDateTime(currentTime.LocalDateTime); if (context.InstructionIndex >= playoutDefinition.Playout.Count)
var timeOnly = TimeOnly.FromDateTime(currentTime.LocalDateTime); {
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 logger.LogInformation("YAML playout instruction handler failed");
dayOnly = dayOnly.AddDays(1);
currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset);
} }
} }
else
if (!instruction.ChangesIndex)
{ {
// this is wrong when offset changes context.InstructionIndex++;
currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset);
} }
} }
return currentTime; return playout;
} }
private async Task<int> GetDaysToBuild() => private async Task<int> GetDaysToBuild() =>
@ -217,81 +112,37 @@ public class YamlPlayoutBuilder(
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild) .GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.IfNoneAsync(2); .IfNoneAsync(2);
private async Task<Option<IMediaCollectionEnumerator>> GetCachedEnumeratorForContent( private static Option<IYamlPlayoutHandler> GetHandlerForInstruction(
YamlPlayoutInitialState initialState, Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers,
Playout playout, EnumeratorCache enumeratorCache,
YamlPlayoutDefinition playoutDefinition, YamlPlayoutInstruction instruction)
Dictionary<string, IMediaCollectionEnumerator> enumerators,
string contentKey,
CancellationToken cancellationToken)
{ {
if (string.IsNullOrWhiteSpace(contentKey)) if (handlers.TryGetValue(instruction, out IYamlPlayoutHandler handler))
{
return Option<IMediaCollectionEnumerator>.None;
}
if (!enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator))
{ {
Option<IMediaCollectionEnumerator> maybeEnumerator = return Optional(handler);
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 Some(enumerator); handler = instruction switch
}
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)
{ {
return Option<IMediaCollectionEnumerator>.None; YamlPlayoutRepeatInstruction => new YamlPlayoutRepeatHandler(),
} YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(),
YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(),
List<MediaItem> items = []; // content handlers
YamlPlayoutCountInstruction => new YamlPlayoutCountHandler(enumeratorCache),
YamlPlayoutContentItem content = playoutDefinition.Content[index]; YamlPlayoutDurationInstruction => new YamlPlayoutDurationHandler(enumeratorCache),
switch (content) YamlPlayoutPadToNextInstruction => new YamlPlayoutPadToNextHandler(enumeratorCache),
{
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 _ => null
initialState.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex); };
var state = new CollectionEnumeratorState { Seed = playout.Seed + index, Index = enumeratorIndex }; if (handler != null)
switch (Enum.Parse<PlaybackOrder>(content.Order, true))
{ {
case PlaybackOrder.Chronological: handlers.Add(instruction, handler);
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; return Optional(handler);
} }
private static async Task<YamlPlayoutDefinition> LoadYamlDefinition(Playout playout, CancellationToken cancellationToken) private static async Task<YamlPlayoutDefinition> LoadYamlDefinition(Playout playout, CancellationToken cancellationToken)

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

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