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 @@ |
|||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutContentGuid |
public class YamlPlayoutContentGuid |
||||||
{ |
{ |
||||||
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutContentItem |
public class YamlPlayoutContentItem |
||||||
{ |
{ |
||||||
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutContentSearchItem : YamlPlayoutContentItem |
public class YamlPlayoutContentSearchItem : YamlPlayoutContentItem |
||||||
{ |
{ |
||||||
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutContentShowItem : YamlPlayoutContentItem |
public class YamlPlayoutContentShowItem : YamlPlayoutContentItem |
||||||
{ |
{ |
||||||
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutCountInstruction : YamlPlayoutInstruction |
public class YamlPlayoutCountInstruction : YamlPlayoutInstruction |
||||||
{ |
{ |
||||||
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; |
||||||
|
|
||||||
public class YamlPlayoutDefinition |
public class YamlPlayoutDefinition |
||||||
{ |
{ |
||||||
@ -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 |
||||||
{ |
{ |
||||||
@ -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)] |
||||||
@ -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 |
||||||
{ |
{ |
||||||
@ -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 |
||||||
{ |
{ |
||||||
@ -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; } |
||||||
} |
} |
||||||
@ -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 |
||||||
{ |
{ |
||||||
@ -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 |
||||||
{ |
{ |
||||||
@ -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); |
|
||||||
} |
|
||||||
} |
|
||||||
Loading…
Reference in new issue