mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
9.4 KiB
243 lines
9.4 KiB
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Interfaces.Metadata; |
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
using ErsatzTV.Core.Interfaces.Scheduling; |
|
using LanguageExt.UnsafeValueAccess; |
|
using Microsoft.Extensions.Logging; |
|
using YamlDotNet.Serialization; |
|
using YamlDotNet.Serialization.NamingConventions; |
|
|
|
namespace ErsatzTV.Core.Scheduling.YamlScheduling; |
|
|
|
public class YamlPlayoutBuilder( |
|
ILocalFileSystem localFileSystem, |
|
IConfigElementRepository configElementRepository, |
|
IMediaCollectionRepository mediaCollectionRepository, |
|
ILogger<YamlPlayoutBuilder> logger) |
|
: IYamlPlayoutBuilder |
|
{ |
|
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) |
|
{ |
|
if (!localFileSystem.FileExists(playout.TemplateFile)) |
|
{ |
|
logger.LogWarning("YAML playout file {File} does not exist; aborting.", playout.TemplateFile); |
|
return playout; |
|
} |
|
|
|
YamlPlayoutDefinition playoutDefinition = await LoadYamlDefinition(playout, cancellationToken); |
|
|
|
DateTimeOffset start = DateTimeOffset.Now; |
|
int daysToBuild = await GetDaysToBuild(); |
|
DateTimeOffset finish = start.AddDays(daysToBuild); |
|
|
|
if (mode is not PlayoutBuildMode.Reset) |
|
{ |
|
logger.LogWarning("YAML playouts can only be reset; ignoring build mode {Mode}", mode.ToString()); |
|
return playout; |
|
} |
|
|
|
// these are only for reset |
|
playout.Seed = new Random().Next(); |
|
playout.Items.Clear(); |
|
|
|
DateTimeOffset currentTime = start; |
|
|
|
// load content and content enumerators on demand |
|
Dictionary<string, IMediaCollectionEnumerator> enumerators = new(); |
|
System.Collections.Generic.HashSet<string> missingContentKeys = []; |
|
|
|
int itemsAfterRepeat = playout.Items.Count; |
|
var index = 0; |
|
while (currentTime < finish) |
|
{ |
|
if (index >= playoutDefinition.Playout.Count) |
|
{ |
|
logger.LogInformation("Reached the end of the YAML playout definition; stopping"); |
|
break; |
|
} |
|
|
|
YamlPlayoutInstruction playoutItem = playoutDefinition.Playout[index]; |
|
|
|
// repeat resets index into YAML playout |
|
if (playoutItem is YamlPlayoutRepeatInstruction) |
|
{ |
|
index = 0; |
|
if (playout.Items.Count == itemsAfterRepeat) |
|
{ |
|
logger.LogWarning("Repeat encountered without adding any playout items; aborting"); |
|
break; |
|
} |
|
|
|
itemsAfterRepeat = playout.Items.Count; |
|
continue; |
|
} |
|
|
|
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetCachedEnumeratorForContent( |
|
playout, |
|
playoutDefinition, |
|
enumerators, |
|
playoutItem.Content, |
|
cancellationToken); |
|
|
|
if (maybeEnumerator.IsNone) |
|
{ |
|
if (!missingContentKeys.Contains(playoutItem.Content)) |
|
{ |
|
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); |
|
missingContentKeys.Add(playoutItem.Content); |
|
} |
|
} |
|
|
|
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) |
|
{ |
|
switch (playoutItem) |
|
{ |
|
case YamlPlayoutCountInstruction count: |
|
currentTime = YamlPlayoutSchedulerCount.Schedule(playout, currentTime, count, enumerator); |
|
break; |
|
case YamlPlayoutDurationInstruction duration: |
|
Option<IMediaCollectionEnumerator> durationFallbackEnumerator = await GetCachedEnumeratorForContent( |
|
playout, |
|
playoutDefinition, |
|
enumerators, |
|
duration.Fallback, |
|
cancellationToken); |
|
currentTime = YamlPlayoutSchedulerDuration.Schedule( |
|
playout, |
|
currentTime, |
|
duration, |
|
enumerator, |
|
durationFallbackEnumerator); |
|
break; |
|
case YamlPlayoutPadToNextInstruction padToNext: |
|
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetCachedEnumeratorForContent( |
|
playout, |
|
playoutDefinition, |
|
enumerators, |
|
padToNext.Fallback, |
|
cancellationToken); |
|
currentTime = YamlPlayoutSchedulerPadToNext.Schedule( |
|
playout, |
|
currentTime, |
|
padToNext, |
|
enumerator, |
|
fallbackEnumerator); |
|
break; |
|
} |
|
} |
|
|
|
index++; |
|
} |
|
|
|
return playout; |
|
} |
|
|
|
private async Task<int> GetDaysToBuild() => |
|
await configElementRepository |
|
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild) |
|
.IfNoneAsync(2); |
|
|
|
private async Task<Option<IMediaCollectionEnumerator>> GetCachedEnumeratorForContent( |
|
Playout playout, |
|
YamlPlayoutDefinition playoutDefinition, |
|
Dictionary<string, IMediaCollectionEnumerator> enumerators, |
|
string contentKey, |
|
CancellationToken cancellationToken) |
|
{ |
|
if (string.IsNullOrWhiteSpace(contentKey)) |
|
{ |
|
return Option<IMediaCollectionEnumerator>.None; |
|
} |
|
|
|
if (!enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator)) |
|
{ |
|
Option<IMediaCollectionEnumerator> maybeEnumerator = |
|
await GetEnumeratorForContent(playout, contentKey, playoutDefinition, cancellationToken); |
|
|
|
if (maybeEnumerator.IsNone) |
|
{ |
|
return Option<IMediaCollectionEnumerator>.None; |
|
} |
|
|
|
foreach (IMediaCollectionEnumerator e in maybeEnumerator) |
|
{ |
|
enumerator = maybeEnumerator.ValueUnsafe(); |
|
enumerators.Add(contentKey, enumerator); |
|
} |
|
} |
|
|
|
return Some(enumerator); |
|
} |
|
|
|
private async Task<Option<IMediaCollectionEnumerator>> GetEnumeratorForContent( |
|
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; |
|
} |
|
|
|
var state = new CollectionEnumeratorState { Seed = playout.Seed + index, Index = 0 }; |
|
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; |
|
} |
|
|
|
private static async Task<YamlPlayoutDefinition> LoadYamlDefinition(Playout playout, CancellationToken cancellationToken) |
|
{ |
|
string yaml = await File.ReadAllTextAsync(playout.TemplateFile, cancellationToken); |
|
|
|
IDeserializer deserializer = new DeserializerBuilder() |
|
.WithNamingConvention(CamelCaseNamingConvention.Instance) |
|
.WithTypeDiscriminatingNodeDeserializer( |
|
o => |
|
{ |
|
var contentKeyMappings = new Dictionary<string, Type> |
|
{ |
|
{ "search", typeof(YamlPlayoutContentSearchItem) }, |
|
{ "show", typeof(YamlPlayoutContentShowItem) } |
|
}; |
|
|
|
o.AddUniqueKeyTypeDiscriminator<YamlPlayoutContentItem>(contentKeyMappings); |
|
|
|
var instructionKeyMappings = new Dictionary<string, Type> |
|
{ |
|
{ "count", typeof(YamlPlayoutCountInstruction) }, |
|
{ "duration", typeof(YamlPlayoutDurationInstruction) }, |
|
{ "pad_to_next", typeof(YamlPlayoutPadToNextInstruction) }, |
|
{ "repeat", typeof(YamlPlayoutRepeatInstruction) } |
|
}; |
|
|
|
o.AddUniqueKeyTypeDiscriminator<YamlPlayoutInstruction>(instructionKeyMappings); |
|
}) |
|
.Build(); |
|
|
|
return deserializer.Deserialize<YamlPlayoutDefinition>(yaml); |
|
} |
|
}
|
|
|