mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* update changelog * refactor some handlers * refactor skip items instruction * more refactoringpull/1821/head
30 changed files with 597 additions and 399 deletions
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutContentGuid |
||||
{ |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutContentItem |
||||
{ |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutContentSearchItem : YamlPlayoutContentItem |
||||
{ |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutContentShowItem : YamlPlayoutContentItem |
||||
{ |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutCountInstruction : YamlPlayoutInstruction |
||||
{ |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||
|
||||
public class YamlPlayoutDefinition |
||||
{ |
@ -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 |
||||
{ |
@ -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)] |
@ -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 |
||||
{ |
@ -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 |
||||
{ |
@ -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; } |
||||
} |
@ -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 |
||||
{ |
@ -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 |
||||
{ |
@ -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; } = []; |
||||
} |
@ -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; } = []; |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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…
Reference in new issue