refactor yaml playout builder (#1820)
* update changelog * refactor some handlers * refactor skip items instruction * more refactoring
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+53
-33
@@ -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 = currentTime.Add(timeSpan);
|
||||
DateTimeOffset targetTime = context.CurrentTime.Add(timeSpan);
|
||||
|
||||
return Schedule(
|
||||
playout,
|
||||
currentTime,
|
||||
targetTime,
|
||||
duration.DiscardAttempts,
|
||||
duration.Trim,
|
||||
GetFillerKind(duration),
|
||||
guideGroup,
|
||||
enumerator,
|
||||
fallbackEnumerator);
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
{
|
||||
// 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
|
||||
playoutItem.Finish = targetTime.UtcDateTime;
|
||||
playoutItem.FillerKind = FillerKind.Fallback;
|
||||
|
||||
playout.Items.Add(playoutItem);
|
||||
context.Playout.Items.Add(playoutItem);
|
||||
fallback.MoveNext();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutContentGuid
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutContentItem
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutContentSearchItem : YamlPlayoutContentItem
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutContentShowItem : YamlPlayoutContentItem
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutCountInstruction : YamlPlayoutInstruction
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutDefinition
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutDurationInstruction : YamlPlayoutInstruction
|
||||
{
|
||||
+3
-1
@@ -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)]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutNewEpgGroupInstruction : YamlPlayoutInstruction
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutPadToNextInstruction : YamlPlayoutInstruction
|
||||
{
|
||||
+3
-1
@@ -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; }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutSkipItemsInstruction : YamlPlayoutInstruction
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
|
||||
|
||||
public class YamlPlayoutWaitUntilInstruction : YamlPlayoutInstruction
|
||||
{
|
||||
@@ -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,263 +37,112 @@ 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)
|
||||
CurrentTime = start,
|
||||
GuideGroup = 1,
|
||||
InstructionIndex = 0
|
||||
};
|
||||
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
if (mode is PlayoutBuildMode.Reset)
|
||||
{
|
||||
context.Playout.Seed = new Random().Next();
|
||||
context.Playout.Items.Clear();
|
||||
|
||||
// handle all on-reset instructions
|
||||
foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
|
||||
{
|
||||
Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(
|
||||
handlers,
|
||||
enumeratorCache,
|
||||
instruction);
|
||||
|
||||
foreach (IYamlPlayoutHandler handler in maybeHandler)
|
||||
{
|
||||
if (!handler.Reset)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Skipping unsupported reset instruction {Instruction}",
|
||||
instruction.GetType().Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
await handler.Handle(context, instruction, logger, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle all playout instructions
|
||||
while (context.CurrentTime < finish)
|
||||
{
|
||||
if (context.InstructionIndex >= playoutDefinition.Playout.Count)
|
||||
{
|
||||
logger.LogInformation("Reached the end of the YAML playout definition; stopping");
|
||||
break;
|
||||
}
|
||||
|
||||
YamlPlayoutInstruction playoutItem = playoutDefinition.Playout[index];
|
||||
YamlPlayoutInstruction instruction = playoutDefinition.Playout[context.InstructionIndex];
|
||||
Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(handlers, enumeratorCache, instruction);
|
||||
|
||||
// handle instructions that don't reference content
|
||||
switch (playoutItem)
|
||||
foreach (IYamlPlayoutHandler handler in maybeHandler)
|
||||
{
|
||||
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);
|
||||
|
||||
if (maybeEnumerator.IsNone)
|
||||
{
|
||||
if (!missingContentKeys.Contains(playoutItem.Content))
|
||||
if (!await handler.Handle(context, instruction, logger, cancellationToken))
|
||||
{
|
||||
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content);
|
||||
missingContentKeys.Add(playoutItem.Content);
|
||||
logger.LogInformation("YAML playout instruction handler failed");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
|
||||
if (!instruction.ChangesIndex)
|
||||
{
|
||||
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;
|
||||
}
|
||||
context.InstructionIndex++;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = skipItems.SkipItems;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return currentTime;
|
||||
}
|
||||
|
||||
private async Task<int> GetDaysToBuild() =>
|
||||
await configElementRepository
|
||||
.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))
|
||||
if (handlers.TryGetValue(instruction, out IYamlPlayoutHandler handler))
|
||||
{
|
||||
return Option<IMediaCollectionEnumerator>.None;
|
||||
return Optional(handler);
|
||||
}
|
||||
|
||||
if (!enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator))
|
||||
handler = instruction switch
|
||||
{
|
||||
Option<IMediaCollectionEnumerator> maybeEnumerator =
|
||||
await GetEnumeratorForContent(initialState, playout, contentKey, playoutDefinition, cancellationToken);
|
||||
YamlPlayoutRepeatInstruction => new YamlPlayoutRepeatHandler(),
|
||||
YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(),
|
||||
YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(),
|
||||
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(),
|
||||
|
||||
if (maybeEnumerator.IsNone)
|
||||
{
|
||||
return Option<IMediaCollectionEnumerator>.None;
|
||||
}
|
||||
// content handlers
|
||||
YamlPlayoutCountInstruction => new YamlPlayoutCountHandler(enumeratorCache),
|
||||
YamlPlayoutDurationInstruction => new YamlPlayoutDurationHandler(enumeratorCache),
|
||||
YamlPlayoutPadToNextInstruction => new YamlPlayoutPadToNextHandler(enumeratorCache),
|
||||
|
||||
foreach (IMediaCollectionEnumerator e in maybeEnumerator)
|
||||
{
|
||||
enumerator = e;
|
||||
enumerators.Add(contentKey, enumerator);
|
||||
}
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
handlers.Add(instruction, 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)
|
||||
{
|
||||
return Option<IMediaCollectionEnumerator>.None;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// start at the appropriate place in the enumerator
|
||||
initialState.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex);
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 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;
|
||||
return Optional(handler);
|
||||
}
|
||||
|
||||
private static async Task<YamlPlayoutDefinition> LoadYamlDefinition(Playout playout, CancellationToken cancellationToken)
|
||||
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
|
||||
public class YamlPlayoutInitialState
|
||||
{
|
||||
public DateTimeOffset CurrentTime { get; set; }
|
||||
|
||||
public Dictionary<string, int> ContentIndex { get; } = [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user