add tests, replace playout items when collections are updated (#1566)

This commit is contained in:
Jason Dove
2024-01-22 10:10:22 -06:00
committed by GitHub
parent 83fc3081d8
commit b88deaafe5
17 changed files with 10366 additions and 33 deletions
@@ -2,6 +2,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
@@ -14,9 +15,15 @@ public class BlockPlayoutBuilder(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
ICollectionEtag collectionEtag,
ILogger<BlockPlayoutBuilder> logger)
: IBlockPlayoutBuilder
{
private static readonly JsonSerializerSettings JsonSettings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public virtual async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{
Logger.LogDebug(
@@ -37,19 +44,29 @@ public class BlockPlayoutBuilder(
int daysToBuild = await GetDaysToBuild();
// get blocks to schedule
List<EffectiveBlock> blocksToSchedule = EffectiveBlock.GetEffectiveBlocks(playout, start, daysToBuild);
List<EffectiveBlock> blocksToSchedule =
EffectiveBlock.GetEffectiveBlocks(playout.Templates, start, daysToBuild);
// get all collection items for the playout
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule);
Map<CollectionKey, string> collectionEtags = GetCollectionEtags(collectionMediaItems);
Dictionary<PlayoutItem, BlockKey> itemBlockKeys =
BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(playout);
// remove items without a block key (shouldn't happen often, just upgrades)
playout.Items.RemoveAll(i => !itemBlockKeys.ContainsKey(i));
// remove old items
// importantly, this should not remove their history
playout.Items.RemoveAll(i => i.FinishOffset < start);
(List<EffectiveBlock> updatedEffectiveBlocks, List<PlayoutItem> playoutItemsToRemove) =
BlockPlayoutChangeDetection.FindUpdatedItems(playout, itemBlockKeys, blocksToSchedule);
BlockPlayoutChangeDetection.FindUpdatedItems(
playout.Items,
itemBlockKeys,
blocksToSchedule,
collectionEtags);
foreach (PlayoutItem playoutItem in playoutItemsToRemove)
{
@@ -119,6 +136,8 @@ public class BlockPlayoutBuilder(
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
var collectionKey = CollectionKey.ForBlockItem(blockItem);
// create a playout item
var playoutItem = new PlayoutItem
{
@@ -134,7 +153,9 @@ public class BlockPlayoutBuilder(
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
//SubtitleMode = scheduleItem.SubtitleMode
BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey)
BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
CollectionEtag = collectionEtags[collectionKey]
};
if (effectiveBlock.Block.StopScheduling is BlockStopScheduling.BeforeDurationEnd
@@ -299,6 +320,19 @@ public class BlockPlayoutBuilder(
return LanguageExt.Map.createRange(tuples);
}
private Map<CollectionKey, string> GetCollectionEtags(
Map<CollectionKey, List<MediaItem>> collectionMediaItems)
{
var result = new Map<CollectionKey, string>();
foreach ((CollectionKey key, List<MediaItem> items) in collectionMediaItems)
{
result = result.Add(key, collectionEtag.ForCollectionItems(items));
}
return result;
}
private static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
MediaVersion version = mediaItem.GetHeadVersion();
@@ -23,22 +23,65 @@ internal static class BlockPlayoutChangeDetection
}
public static Tuple<List<EffectiveBlock>, List<PlayoutItem>> FindUpdatedItems(
Playout playout,
List<PlayoutItem> playoutItems,
Dictionary<PlayoutItem, BlockKey> itemBlockKeys,
List<EffectiveBlock> blocksToSchedule)
List<EffectiveBlock> blocksToSchedule,
Map<CollectionKey, string> collectionEtags)
{
DateTimeOffset lastScheduledItem = playout.Items.Count == 0
DateTimeOffset lastScheduledItem = playoutItems.Count == 0
? SystemTime.MinValueUtc
: playout.Items.Max(i => i.StartOffset);
: playoutItems.Max(i => i.StartOffset);
var existingBlockKeys = itemBlockKeys.Values.ToImmutableHashSet();
var blockKeysToSchedule = blocksToSchedule.Map(b => b.BlockKey).ToImmutableHashSet();
var updatedBlocks = new List<EffectiveBlock>();
var updatedItems = new List<PlayoutItem>();
var updatedBlocks = new System.Collections.Generic.HashSet<EffectiveBlock>();
var updatedItemIds = new System.Collections.Generic.HashSet<int>();
var earliestEffectiveBlocks = new Dictionary<BlockKey, DateTimeOffset>();
var earliestBlocks = new Dictionary<int, DateTimeOffset>();
// check for changed collections
foreach (EffectiveBlock effectiveBlock in blocksToSchedule.OrderBy(b => b.Start))
{
foreach (PlayoutItem playoutItem in playoutItems)
{
int blockId = itemBlockKeys[playoutItem].b;
if (effectiveBlock.Block.Id != blockId)
{
continue;
}
bool isUpdated = string.IsNullOrWhiteSpace(playoutItem.CollectionKey);
if (!isUpdated)
{
CollectionKey collectionKey = JsonConvert.DeserializeObject<CollectionKey>(playoutItem.CollectionKey);
// collection is no longer present or collection has been modified
isUpdated = !collectionEtags.ContainsKey(collectionKey) ||
collectionEtags[collectionKey] != playoutItem.CollectionEtag;
}
if (isUpdated)
{
// playout item needs to be removed/re-added
updatedItemIds.Add(playoutItem.Id);
// block needs to be scheduled again
updatedBlocks.Add(effectiveBlock);
if (!earliestEffectiveBlocks.ContainsKey(effectiveBlock.BlockKey))
{
earliestEffectiveBlocks[effectiveBlock.BlockKey] = effectiveBlock.Start;
}
if (!earliestBlocks.ContainsKey(effectiveBlock.Block.Id))
{
earliestBlocks[effectiveBlock.Block.Id] = effectiveBlock.Start;
}
}
}
}
// process in sorted order to simplify checks
foreach (EffectiveBlock effectiveBlock in blocksToSchedule.OrderBy(b => b.Start))
{
@@ -80,7 +123,7 @@ internal static class BlockPlayoutChangeDetection
}
// find affected playout items
foreach (PlayoutItem item in playout.Items)
foreach (PlayoutItem item in playoutItems)
{
BlockKey blockKey = itemBlockKeys[item];
@@ -92,11 +135,11 @@ internal static class BlockPlayoutChangeDetection
if (!blockKeysToSchedule.Contains(blockKey) || blockKeyIsAffected || blockIdIsAffected)
{
updatedItems.Add(item);
updatedItemIds.Add(item.Id);
}
}
return Tuple(updatedBlocks, updatedItems);
return Tuple(updatedBlocks.ToList(), playoutItems.Filter(i => updatedItemIds.Contains(i.Id)).ToList());
}
public static void RemoveItemAndHistory(Playout playout, PlayoutItem playoutItem)
@@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
@@ -12,11 +13,13 @@ public class BlockPlayoutPreviewBuilder(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
ICollectionEtag collectionEtag,
ILogger<BlockPlayoutBuilder> logger) : BlockPlayoutBuilder(
configElementRepository,
mediaCollectionRepository,
televisionRepository,
artistRepository,
collectionEtag,
logger), IBlockPlayoutPreviewBuilder
{
private readonly Dictionary<Guid, System.Collections.Generic.HashSet<CollectionKey>> _randomizedCollections = [];
@@ -1,11 +1,13 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Scheduling.BlockScheduling;
internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset Start)
{
public static List<EffectiveBlock> GetEffectiveBlocks(Playout playout, DateTimeOffset start, int daysToBuild)
public static List<EffectiveBlock> GetEffectiveBlocks(
ICollection<PlayoutTemplate> templates,
DateTimeOffset start,
int daysToBuild)
{
DateTimeOffset finish = start.AddDays(daysToBuild);
@@ -13,31 +15,21 @@ internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset St
DateTimeOffset current = start.Date;
while (current < finish)
{
foreach (PlayoutTemplate playoutTemplate in PlayoutTemplateSelector.GetPlayoutTemplateFor(
playout.Templates,
current))
Option<PlayoutTemplate> maybeTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(templates, current);
foreach (PlayoutTemplate playoutTemplate in maybeTemplate)
{
// logger.LogDebug(
// "Will schedule day {Date} using template {Template}",
// current,
// playoutTemplate.Template.Name);
foreach (TemplateItem templateItem in playoutTemplate.Template.Items)
{
var effectiveBlock = new EffectiveBlock(
templateItem.Block,
new BlockKey(templateItem.Block, templateItem.Template, playoutTemplate),
new DateTimeOffset(
current.Year,
current.Month,
current.Day,
templateItem.StartTime.Hours,
templateItem.StartTime.Minutes,
0,
start.Offset));
DateTimeOffset today = current;
var newBlocks = playoutTemplate.Template.Items
.Map(i => ToEffectiveBlock(playoutTemplate, i, today, start))
.ToList();
effectiveBlocks.Add(effectiveBlock);
}
effectiveBlocks.AddRange(newBlocks);
}
current = current.AddDays(1);
@@ -48,4 +40,21 @@ internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset St
return effectiveBlocks;
}
private static EffectiveBlock ToEffectiveBlock(
PlayoutTemplate playoutTemplate,
TemplateItem templateItem,
DateTimeOffset current,
DateTimeOffset start) =>
new(
templateItem.Block,
new BlockKey(templateItem.Block, templateItem.Template, playoutTemplate),
new DateTimeOffset(
current.Year,
current.Month,
current.Day,
templateItem.StartTime.Hours,
templateItem.StartTime.Minutes,
0,
start.Offset));
}
@@ -92,6 +92,7 @@ internal static class HistoryDetails
maybeMatchedItem = fakeItem;
}
}
// TODO: match media item
foreach (MediaItem matchedItem in maybeMatchedItem)
{