add tests, replace playout items when collections are updated (#1566)
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user