Browse Source

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

pull/1567/head
Jason Dove 2 years ago committed by GitHub
parent
commit
b88deaafe5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 172
      ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutChangeDetectionTests.cs
  2. 109
      ErsatzTV.Core.Tests/Scheduling/BlockScheduling/EffectiveBlockTests.cs
  3. 2
      ErsatzTV.Core/Domain/PlayoutItem.cs
  4. 8
      ErsatzTV.Core/Interfaces/Metadata/ICollectionEtag.cs
  5. 40
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  6. 61
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutChangeDetection.cs
  7. 3
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs
  8. 51
      ErsatzTV.Core/Scheduling/BlockScheduling/EffectiveBlock.cs
  9. 1
      ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs
  10. 4916
      ErsatzTV.Infrastructure.MySql/Migrations/20240122152005_Add_PlayoutItemCollectionKey.Designer.cs
  11. 40
      ErsatzTV.Infrastructure.MySql/Migrations/20240122152005_Add_PlayoutItemCollectionKey.cs
  12. 6
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  13. 4914
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240122144648_Add_PlayoutItemCollectionKey.Designer.cs
  14. 38
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240122144648_Add_PlayoutItemCollectionKey.cs
  15. 6
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  16. 31
      ErsatzTV.Infrastructure/Metadata/CollectionEtag.cs
  17. 1
      ErsatzTV/Startup.cs

172
ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutChangeDetectionTests.cs

@ -0,0 +1,172 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Core.Scheduling.BlockScheduling;
using FluentAssertions;
using Newtonsoft.Json;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling.BlockScheduling;
public static class BlockPlayoutChangeDetectionTests
{
[TestFixture]
public class FindUpdatedItems
{
// takes playout items, item block keys and effective blocks
// returns blocks to schedule and playout items to remove
// test case: nothing has changed
// test case: block has moved from one time to another time, nothing after
// test case: block has moved from one time to another time, same block after
// test case: block has moved from one time to another time, different block with same collection after
// test case: block was moved from one time to another time, different block with different collection after
// test case: block was removed, nothing after
// test case: block was removed, same block after
// test case: block was removed, different block with same collection after
// test case: block was removed, different block with different collection after
// test case: block was added, nothing after
// test case: block was added, same block after
// test case: block was added, different block with same collection after
// test case: block was added, different block with different collection after
[Test]
public void Should_Work_When_Nothing_Has_Changed()
{
DateTimeOffset dateUpdated = DateTimeOffset.Now;
List<Block> blocks = Blocks(dateUpdated);
var template = new Template { Id = 1, DateUpdated = dateUpdated.UtcDateTime };
var playoutTemplate = new PlayoutTemplate { Id = 10, DateUpdated = dateUpdated.UtcDateTime };
Block block1 = blocks[0];
Block block2 = blocks[1];
var blockKey1 = new BlockKey(block1, template, playoutTemplate);
var blockKey2 = new BlockKey(block2, template, playoutTemplate);
var collectionKey1 = CollectionKey.ForBlockItem(block1.Items.Head());
var collectionKey2 = CollectionKey.ForBlockItem(block2.Items.Head());
// 9am-9:20am
PlayoutItem playoutItem1 = PlayoutItem(blockKey1, collectionKey1, GetLocalDate(2024, 1, 17).AddHours(9));
// 1pm-1:20pm
PlayoutItem playoutItem2 = PlayoutItem(blockKey2, collectionKey2, GetLocalDate(2024, 1, 17).AddHours(13));
List<PlayoutItem> playoutItems = [playoutItem1, playoutItem2];
Dictionary<PlayoutItem, BlockKey> itemBlockKeys =
new()
{
{ playoutItem1, blockKey1 },
{ playoutItem2, blockKey2 }
};
List<EffectiveBlock> effectiveBlocks =
[
new EffectiveBlock(block1, blockKey1, GetLocalDate(2024, 1, 17).AddHours(9)),
new EffectiveBlock(block2, blockKey2, GetLocalDate(2024, 1, 17).AddHours(13)),
];
Map<CollectionKey, string> collectionEtags = LanguageExt.Map<CollectionKey, string>.Empty;
collectionEtags = collectionEtags.Add(collectionKey1, JsonConvert.SerializeObject(collectionKey1));
collectionEtags = collectionEtags.Add(collectionKey2, JsonConvert.SerializeObject(collectionKey2));
Tuple<List<EffectiveBlock>, List<PlayoutItem>> result = BlockPlayoutChangeDetection.FindUpdatedItems(
playoutItems,
itemBlockKeys,
effectiveBlocks,
collectionEtags);
// nothing to schedule
result.Item1.Should().HaveCount(0);
// do not need to remove any playout items or history
result.Item2.Should().HaveCount(0);
}
private static List<Block> Blocks(DateTimeOffset dateUpdated)
{
List<Block> blocks =
[
// SHOW A
new Block
{
Id = 1,
Items =
[
new BlockItem
{
Id = 1,
Index = 1,
BlockId = 1,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow,
MediaItemId = 1,
PlaybackOrder = PlaybackOrder.Chronological
}
],
DateUpdated = dateUpdated.UtcDateTime
},
// SHOW B
new Block
{
Id = 2,
Items =
[
new BlockItem
{
Id = 2,
Index = 1,
BlockId = 2,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow,
MediaItemId = 2,
PlaybackOrder = PlaybackOrder.Chronological
}
],
DateUpdated = dateUpdated.UtcDateTime
},
// SHOW C
new Block
{
Id = 3,
Items =
[
new BlockItem
{
Id = 3,
Index = 1,
BlockId = 3,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow,
MediaItemId = 3,
PlaybackOrder = PlaybackOrder.Chronological
}
],
DateUpdated = dateUpdated.UtcDateTime
}
];
foreach (Block block in blocks)
{
foreach (BlockItem blockItem in block.Items)
{
blockItem.Block = block;
}
}
return blocks;
}
private static PlayoutItem PlayoutItem(BlockKey blockKey, CollectionKey collectionKey, DateTimeOffset start) =>
new()
{
Start = start.UtcDateTime,
Finish = start.UtcDateTime.AddMinutes(20),
BlockKey = JsonConvert.SerializeObject(blockKey),
CollectionKey = JsonConvert.SerializeObject(collectionKey),
CollectionEtag = JsonConvert.SerializeObject(collectionKey)
};
private static DateTimeOffset GetLocalDate(int year, int month, int day) =>
new(year, month, day, 0, 0, 0, TimeSpan.FromHours(-6));
}
}

109
ErsatzTV.Core.Tests/Scheduling/BlockScheduling/EffectiveBlockTests.cs

@ -0,0 +1,109 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling.BlockScheduling;
using FluentAssertions;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling.BlockScheduling;
public static class EffectiveBlockTests
{
[TestFixture]
public class GetEffectiveBlocks
{
[Test]
public void Should_Work_With_No_Matching_Days()
{
DateTimeOffset now = DateTimeOffset.Now;
List<PlayoutTemplate> templates =
[
new PlayoutTemplate
{
Index = 1,
DaysOfWeek = [DayOfWeek.Sunday],
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
Template = SingleBlockTemplate(now),
DateUpdated = now.UtcDateTime
}
];
DateTimeOffset start = GetLocalDate(2024, 1, 15).AddHours(9);
List<EffectiveBlock> result = EffectiveBlock.GetEffectiveBlocks(templates, start, daysToBuild: 5);
result.Should().HaveCount(0);
}
[Test]
public void Should_Work_With_Blank_Days()
{
DateTimeOffset now = DateTimeOffset.Now;
List<PlayoutTemplate> templates =
[
new PlayoutTemplate
{
Index = 1,
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday],
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
Template = SingleBlockTemplate(now),
DateUpdated = now.UtcDateTime
}
];
DateTimeOffset start = GetLocalDate(2024, 1, 15).AddHours(9);
List<EffectiveBlock> result = EffectiveBlock.GetEffectiveBlocks(templates, start, daysToBuild: 5);
result.Should().HaveCount(3);
result[0].Start.DayOfWeek.Should().Be(DayOfWeek.Monday);
result[0].Start.Date.Should().Be(GetLocalDate(2024, 1, 15).Date);
result[1].Start.DayOfWeek.Should().Be(DayOfWeek.Wednesday);
result[1].Start.Date.Should().Be(GetLocalDate(2024, 1, 17).Date);
result[2].Start.DayOfWeek.Should().Be(DayOfWeek.Friday);
result[2].Start.Date.Should().Be(GetLocalDate(2024, 1, 19).Date);
}
// TODO: test when clocks spring forward
// TODO: test when clocks fall back
// TODO: offset may be incorrect on days with time change, since start offset is re-used
}
private static DateTimeOffset GetLocalDate(int year, int month, int day) =>
new(year, month, day, 0, 0, 0, TimeSpan.FromHours(-6));
private static Template SingleBlockTemplate(DateTimeOffset dateUpdated)
{
var template = new Template
{
Id = 1,
Items =
[
new TemplateItem
{
Block = new Block
{
Id = 1,
DateUpdated = dateUpdated.UtcDateTime
},
StartTime = TimeSpan.FromHours(9)
}
],
DateUpdated = dateUpdated.UtcDateTime
};
// this is used for navigation
foreach (TemplateItem item in template.Items)
{
item.Template = template;
}
return template;
}
}

2
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -28,6 +28,8 @@ public class PlayoutItem
public string PreferredSubtitleLanguageCode { get; set; } public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; } public ChannelSubtitleMode? SubtitleMode { get; set; }
public string BlockKey { get; set; } public string BlockKey { get; set; }
public string CollectionKey { get; set; }
public string CollectionEtag { get; set; }
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();

8
ErsatzTV.Core/Interfaces/Metadata/ICollectionEtag.cs

@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata;
public interface ICollectionEtag
{
string ForCollectionItems(List<MediaItem> items);
}

40
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs

@ -2,6 +2,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -14,9 +15,15 @@ public class BlockPlayoutBuilder(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
IArtistRepository artistRepository, IArtistRepository artistRepository,
ICollectionEtag collectionEtag,
ILogger<BlockPlayoutBuilder> logger) ILogger<BlockPlayoutBuilder> logger)
: IBlockPlayoutBuilder : IBlockPlayoutBuilder
{ {
private static readonly JsonSerializerSettings JsonSettings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public virtual async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) public virtual async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{ {
Logger.LogDebug( Logger.LogDebug(
@ -37,10 +44,12 @@ public class BlockPlayoutBuilder(
int daysToBuild = await GetDaysToBuild(); int daysToBuild = await GetDaysToBuild();
// get blocks to schedule // 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 // get all collection items for the playout
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule); Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule);
Map<CollectionKey, string> collectionEtags = GetCollectionEtags(collectionMediaItems);
Dictionary<PlayoutItem, BlockKey> itemBlockKeys = Dictionary<PlayoutItem, BlockKey> itemBlockKeys =
BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(playout); BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(playout);
@ -48,8 +57,16 @@ public class BlockPlayoutBuilder(
// remove items without a block key (shouldn't happen often, just upgrades) // remove items without a block key (shouldn't happen often, just upgrades)
playout.Items.RemoveAll(i => !itemBlockKeys.ContainsKey(i)); 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) = (List<EffectiveBlock> updatedEffectiveBlocks, List<PlayoutItem> playoutItemsToRemove) =
BlockPlayoutChangeDetection.FindUpdatedItems(playout, itemBlockKeys, blocksToSchedule); BlockPlayoutChangeDetection.FindUpdatedItems(
playout.Items,
itemBlockKeys,
blocksToSchedule,
collectionEtags);
foreach (PlayoutItem playoutItem in playoutItemsToRemove) foreach (PlayoutItem playoutItem in playoutItemsToRemove)
{ {
@ -119,6 +136,8 @@ public class BlockPlayoutBuilder(
TimeSpan itemDuration = DurationForMediaItem(mediaItem); TimeSpan itemDuration = DurationForMediaItem(mediaItem);
var collectionKey = CollectionKey.ForBlockItem(blockItem);
// create a playout item // create a playout item
var playoutItem = new PlayoutItem var playoutItem = new PlayoutItem
{ {
@ -134,7 +153,9 @@ public class BlockPlayoutBuilder(
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle, //PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, //PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
//SubtitleMode = scheduleItem.SubtitleMode //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 if (effectiveBlock.Block.StopScheduling is BlockStopScheduling.BeforeDurationEnd
@ -299,6 +320,19 @@ public class BlockPlayoutBuilder(
return LanguageExt.Map.createRange(tuples); 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) private static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{ {
MediaVersion version = mediaItem.GetHeadVersion(); MediaVersion version = mediaItem.GetHeadVersion();

61
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutChangeDetection.cs

@ -23,22 +23,65 @@ internal static class BlockPlayoutChangeDetection
} }
public static Tuple<List<EffectiveBlock>, List<PlayoutItem>> FindUpdatedItems( public static Tuple<List<EffectiveBlock>, List<PlayoutItem>> FindUpdatedItems(
Playout playout, List<PlayoutItem> playoutItems,
Dictionary<PlayoutItem, BlockKey> itemBlockKeys, 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 ? SystemTime.MinValueUtc
: playout.Items.Max(i => i.StartOffset); : playoutItems.Max(i => i.StartOffset);
var existingBlockKeys = itemBlockKeys.Values.ToImmutableHashSet(); var existingBlockKeys = itemBlockKeys.Values.ToImmutableHashSet();
var blockKeysToSchedule = blocksToSchedule.Map(b => b.BlockKey).ToImmutableHashSet(); var blockKeysToSchedule = blocksToSchedule.Map(b => b.BlockKey).ToImmutableHashSet();
var updatedBlocks = new List<EffectiveBlock>(); var updatedBlocks = new System.Collections.Generic.HashSet<EffectiveBlock>();
var updatedItems = new List<PlayoutItem>(); var updatedItemIds = new System.Collections.Generic.HashSet<int>();
var earliestEffectiveBlocks = new Dictionary<BlockKey, DateTimeOffset>(); var earliestEffectiveBlocks = new Dictionary<BlockKey, DateTimeOffset>();
var earliestBlocks = new Dictionary<int, 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 // process in sorted order to simplify checks
foreach (EffectiveBlock effectiveBlock in blocksToSchedule.OrderBy(b => b.Start)) foreach (EffectiveBlock effectiveBlock in blocksToSchedule.OrderBy(b => b.Start))
{ {
@ -80,7 +123,7 @@ internal static class BlockPlayoutChangeDetection
} }
// find affected playout items // find affected playout items
foreach (PlayoutItem item in playout.Items) foreach (PlayoutItem item in playoutItems)
{ {
BlockKey blockKey = itemBlockKeys[item]; BlockKey blockKey = itemBlockKeys[item];
@ -92,11 +135,11 @@ internal static class BlockPlayoutChangeDetection
if (!blockKeysToSchedule.Contains(blockKey) || blockKeyIsAffected || blockIdIsAffected) 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) public static void RemoveItemAndHistory(Playout playout, PlayoutItem playoutItem)

3
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs

@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -12,11 +13,13 @@ public class BlockPlayoutPreviewBuilder(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
IArtistRepository artistRepository, IArtistRepository artistRepository,
ICollectionEtag collectionEtag,
ILogger<BlockPlayoutBuilder> logger) : BlockPlayoutBuilder( ILogger<BlockPlayoutBuilder> logger) : BlockPlayoutBuilder(
configElementRepository, configElementRepository,
mediaCollectionRepository, mediaCollectionRepository,
televisionRepository, televisionRepository,
artistRepository, artistRepository,
collectionEtag,
logger), IBlockPlayoutPreviewBuilder logger), IBlockPlayoutPreviewBuilder
{ {
private readonly Dictionary<Guid, System.Collections.Generic.HashSet<CollectionKey>> _randomizedCollections = []; private readonly Dictionary<Guid, System.Collections.Generic.HashSet<CollectionKey>> _randomizedCollections = [];

51
ErsatzTV.Core/Scheduling/BlockScheduling/EffectiveBlock.cs

@ -1,11 +1,13 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Scheduling.BlockScheduling; namespace ErsatzTV.Core.Scheduling.BlockScheduling;
internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset Start) 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); DateTimeOffset finish = start.AddDays(daysToBuild);
@ -13,31 +15,21 @@ internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset St
DateTimeOffset current = start.Date; DateTimeOffset current = start.Date;
while (current < finish) while (current < finish)
{ {
foreach (PlayoutTemplate playoutTemplate in PlayoutTemplateSelector.GetPlayoutTemplateFor( Option<PlayoutTemplate> maybeTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(templates, current);
playout.Templates, foreach (PlayoutTemplate playoutTemplate in maybeTemplate)
current))
{ {
// logger.LogDebug( // logger.LogDebug(
// "Will schedule day {Date} using template {Template}", // "Will schedule day {Date} using template {Template}",
// current, // current,
// playoutTemplate.Template.Name); // playoutTemplate.Template.Name);
foreach (TemplateItem templateItem in playoutTemplate.Template.Items) DateTimeOffset today = current;
{
var effectiveBlock = new EffectiveBlock( var newBlocks = playoutTemplate.Template.Items
templateItem.Block, .Map(i => ToEffectiveBlock(playoutTemplate, i, today, start))
new BlockKey(templateItem.Block, templateItem.Template, playoutTemplate), .ToList();
new DateTimeOffset(
current.Year, effectiveBlocks.AddRange(newBlocks);
current.Month,
current.Day,
templateItem.StartTime.Hours,
templateItem.StartTime.Minutes,
0,
start.Offset));
effectiveBlocks.Add(effectiveBlock);
}
} }
current = current.AddDays(1); current = current.AddDays(1);
@ -48,4 +40,21 @@ internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset St
return effectiveBlocks; 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));
} }

1
ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs

@ -92,6 +92,7 @@ internal static class HistoryDetails
maybeMatchedItem = fakeItem; maybeMatchedItem = fakeItem;
} }
} }
// TODO: match media item
foreach (MediaItem matchedItem in maybeMatchedItem) foreach (MediaItem matchedItem in maybeMatchedItem)
{ {

4916
ErsatzTV.Infrastructure.MySql/Migrations/20240122152005_Add_PlayoutItemCollectionKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

40
ErsatzTV.Infrastructure.MySql/Migrations/20240122152005_Add_PlayoutItemCollectionKey.cs

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutItemCollectionKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CollectionEtag",
table: "PlayoutItem",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "CollectionKey",
table: "PlayoutItem",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CollectionEtag",
table: "PlayoutItem");
migrationBuilder.DropColumn(
name: "CollectionKey",
table: "PlayoutItem");
}
}
}

6
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -1443,6 +1443,12 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("ChapterTitle") b.Property<string>("ChapterTitle")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("CollectionEtag")
.HasColumnType("longtext");
b.Property<string>("CollectionKey")
.HasColumnType("longtext");
b.Property<string>("CustomTitle") b.Property<string>("CustomTitle")
.HasColumnType("longtext"); .HasColumnType("longtext");

4914
ErsatzTV.Infrastructure.Sqlite/Migrations/20240122144648_Add_PlayoutItemCollectionKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

38
ErsatzTV.Infrastructure.Sqlite/Migrations/20240122144648_Add_PlayoutItemCollectionKey.cs

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutItemCollectionKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CollectionEtag",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CollectionKey",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CollectionEtag",
table: "PlayoutItem");
migrationBuilder.DropColumn(
name: "CollectionKey",
table: "PlayoutItem");
}
}
}

6
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -1441,6 +1441,12 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("ChapterTitle") b.Property<string>("ChapterTitle")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("CollectionEtag")
.HasColumnType("TEXT");
b.Property<string>("CollectionKey")
.HasColumnType("TEXT");
b.Property<string>("CustomTitle") b.Property<string>("CustomTitle")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

31
ErsatzTV.Infrastructure/Metadata/CollectionEtag.cs

@ -0,0 +1,31 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using Microsoft.IO;
namespace ErsatzTV.Infrastructure.Metadata;
[SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms")]
public class CollectionEtag : ICollectionEtag
{
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public CollectionEtag(RecyclableMemoryStreamManager recyclableMemoryStreamManager) =>
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
public string ForCollectionItems(List<MediaItem> items)
{
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
using var bw = new BinaryWriter(ms);
foreach (MediaItem item in items.OrderBy(i => i.Id))
{
bw.Write(item.Id);
}
ms.Position = 0;
byte[] hash = SHA1.Create().ComputeHash(ms);
return BitConverter.ToString(hash).Replace("-", string.Empty);
}
}

1
ErsatzTV/Startup.cs

@ -602,6 +602,7 @@ public class Startup
services.AddScoped<ILibraryRepository, LibraryRepository>(); services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddScoped<IMetadataRepository, MetadataRepository>(); services.AddScoped<IMetadataRepository, MetadataRepository>();
services.AddScoped<IArtworkRepository, ArtworkRepository>(); services.AddScoped<IArtworkRepository, ArtworkRepository>();
services.AddScoped<ICollectionEtag, CollectionEtag>();
services.AddScoped<IFFmpegLocator, FFmpegLocator>(); services.AddScoped<IFFmpegLocator, FFmpegLocator>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>(); services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>(); services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();

Loading…
Cancel
Save