Stream custom live channels using your own media
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.
 
 
 
 

650 lines
21 KiB

using Destructurama;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Core.Scheduling.BlockScheduling;
using ErsatzTV.Core.Tests.Fakes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Serilog;
using Shouldly;
namespace ErsatzTV.Core.Tests.Scheduling.BlockScheduling;
public class BlockPlayoutBuilderTests
{
private readonly ILogger<BlockPlayoutBuilder> _logger;
public BlockPlayoutBuilderTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.Destructure.UsingAttributes()
.CreateLogger();
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
_logger = loggerFactory.CreateLogger<BlockPlayoutBuilder>();
}
[TestFixture]
public class Build : BlockPlayoutBuilderTests
{
[Test]
[CancelAfter(10_000)]
public async Task Should_Start_At_Beginning_Of_Current_Block(CancellationToken cancellationToken)
{
var collection = new SmartCollection
{
Id = 1,
Query = "asdf"
};
var block = new Block
{
Id = 1,
Name = "Test Block",
Minutes = 30,
Items =
[
new BlockItem
{
Id = 1,
CollectionType = CollectionType.SmartCollection,
PlaybackOrder = PlaybackOrder.Chronological,
Index = 1,
SmartCollection = collection,
SmartCollectionId = collection.Id
}
],
StopScheduling = BlockStopScheduling.AfterDurationEnd
};
var template = new Template
{
Id = 1,
Items = []
};
var templateItem = new TemplateItem
{
Block = block,
BlockId = block.Id,
StartTime = TimeSpan.FromHours(9),
Template = template,
TemplateId = template.Id
};
template.Items.Add(templateItem);
var playoutTemplate = new PlayoutTemplate
{
Id = 1,
Index = 1,
Template = template,
TemplateId = template.Id,
DaysOfMonth = AlternateScheduleSelector.AllDaysOfMonth(),
DaysOfWeek = AlternateScheduleSelector.AllDaysOfWeek(),
MonthsOfYear = AlternateScheduleSelector.AllMonthsOfYear()
};
var playout = new Playout
{
Id = 1,
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
Templates =
[
playoutTemplate
],
Items = [],
PlayoutHistory = []
};
var now = HoursAfterMidnight(9).AddMinutes(15);
var mediaItems = new List<MediaItem>
{
new Movie
{
Id = 1,
MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today }],
MediaVersions =
[
new MediaVersion
{
Duration = TimeSpan.FromMinutes(25),
MediaFiles = [new MediaFile { Path = "/fake/path/1" }]
}
]
}
};
var collectionRepo = new FakeMediaCollectionRepository(
Map((collection.Id, mediaItems))
);
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
configRepo
.GetValue<int>(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any<CancellationToken>())
.Returns(Some(1));
var builder = new BlockPlayoutBuilder(
configRepo,
collectionRepo,
Substitute.For<ITelevisionRepository>(),
Substitute.For<IArtistRepository>(),
Substitute.For<ICollectionEtag>(),
_logger);
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
playout.Templates.ToList(),
null,
[],
[],
TimeSpan.Zero);
Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now,
playout,
referenceData,
PlayoutBuildMode.Reset,
cancellationToken);
buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
}
[Test]
[CancelAfter(10_000)]
public async Task Should_Discard_Item_That_Will_Never_Fit(CancellationToken cancellationToken)
{
var collection = new SmartCollection
{
Id = 1,
Query = "asdf"
};
var block = new Block
{
Id = 1,
Name = "Test Block",
Minutes = 30,
Items =
[
new BlockItem
{
Id = 1,
CollectionType = CollectionType.SmartCollection,
PlaybackOrder = PlaybackOrder.Chronological,
Index = 1,
SmartCollection = collection,
SmartCollectionId = collection.Id
}
],
StopScheduling = BlockStopScheduling.BeforeDurationEnd
};
var template = new Template
{
Id = 1,
Items = []
};
var templateItem = new TemplateItem
{
Block = block,
BlockId = block.Id,
StartTime = TimeSpan.FromHours(9),
Template = template,
TemplateId = template.Id
};
template.Items.Add(templateItem);
var playoutTemplate = new PlayoutTemplate
{
Id = 1,
Index = 1,
Template = template,
TemplateId = template.Id,
DaysOfMonth = AlternateScheduleSelector.AllDaysOfMonth(),
DaysOfWeek = AlternateScheduleSelector.AllDaysOfWeek(),
MonthsOfYear = AlternateScheduleSelector.AllMonthsOfYear()
};
var playout = new Playout
{
Id = 1,
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
Templates =
[
playoutTemplate
],
Items = [],
PlayoutHistory = []
};
var now = HoursAfterMidnight(9).AddMinutes(15);
var mediaItems = new List<MediaItem>
{
new Movie
{
Id = 1,
MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today }],
MediaVersions =
[
new MediaVersion
{
Duration = TimeSpan.FromHours(1),
MediaFiles = [new MediaFile { Path = "/fake/path/1" }]
}
]
},
new Movie
{
Id = 2,
MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today.AddDays(1) }],
MediaVersions =
[
new MediaVersion
{
Duration = TimeSpan.FromMinutes(25),
MediaFiles = [new MediaFile { Path = "/fake/path/2" }]
}
]
}
};
var collectionRepo = new FakeMediaCollectionRepository(
Map((collection.Id, mediaItems))
);
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
configRepo
.GetValue<int>(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any<CancellationToken>())
.Returns(Some(1));
var builder = new BlockPlayoutBuilder(
configRepo,
collectionRepo,
Substitute.For<ITelevisionRepository>(),
Substitute.For<IArtistRepository>(),
Substitute.For<ICollectionEtag>(),
_logger);
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
playout.Templates.ToList(),
null,
[],
[],
TimeSpan.Zero);
Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now,
playout,
referenceData,
PlayoutBuildMode.Reset,
cancellationToken);
buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
}
[Test]
[CancelAfter(10_000)]
public async Task Should_Discard_Collection_That_Will_Never_Fit(CancellationToken cancellationToken)
{
var collection = new SmartCollection
{
Id = 1,
Query = "asdf"
};
var collection2 = new SmartCollection
{
Id = 2,
Query = "asdf2"
};
var block = new Block
{
Id = 1,
Name = "Test Block",
Minutes = 30,
Items =
[
new BlockItem
{
Id = 1,
CollectionType = CollectionType.SmartCollection,
PlaybackOrder = PlaybackOrder.Chronological,
Index = 1,
SmartCollection = collection,
SmartCollectionId = collection.Id
},
new BlockItem
{
Id = 2,
CollectionType = CollectionType.SmartCollection,
PlaybackOrder = PlaybackOrder.Chronological,
Index = 2,
SmartCollection = collection2,
SmartCollectionId = collection2.Id
}
],
StopScheduling = BlockStopScheduling.BeforeDurationEnd
};
var template = new Template
{
Id = 1,
Items = []
};
var templateItem = new TemplateItem
{
Block = block,
BlockId = block.Id,
StartTime = TimeSpan.FromHours(9),
Template = template,
TemplateId = template.Id
};
template.Items.Add(templateItem);
var playoutTemplate = new PlayoutTemplate
{
Id = 1,
Index = 1,
Template = template,
TemplateId = template.Id,
DaysOfMonth = AlternateScheduleSelector.AllDaysOfMonth(),
DaysOfWeek = AlternateScheduleSelector.AllDaysOfWeek(),
MonthsOfYear = AlternateScheduleSelector.AllMonthsOfYear()
};
var playout = new Playout
{
Id = 1,
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
Templates =
[
playoutTemplate
],
Items = [],
PlayoutHistory = []
};
var now = HoursAfterMidnight(9).AddMinutes(15);
var mediaItems = new List<MediaItem>
{
new Movie
{
Id = 1,
MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today }],
MediaVersions =
[
new MediaVersion
{
Duration = TimeSpan.FromHours(1),
MediaFiles = [new MediaFile { Path = "/fake/path/1" }]
}
]
}
};
var mediaItems2 = new List<MediaItem>
{
new Movie
{
Id = 2,
MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today.AddDays(1) }],
MediaVersions =
[
new MediaVersion
{
Duration = TimeSpan.FromMinutes(25),
MediaFiles = [new MediaFile { Path = "/fake/path/2" }]
}
]
}
};
var collectionRepo = new FakeMediaCollectionRepository(
new Map<int, List<MediaItem>>(
[
(collection.Id, mediaItems),
(collection2.Id, mediaItems2)
])
);
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
configRepo
.GetValue<int>(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any<CancellationToken>())
.Returns(Some(1));
var builder = new BlockPlayoutBuilder(
configRepo,
collectionRepo,
Substitute.For<ITelevisionRepository>(),
Substitute.For<IArtistRepository>(),
Substitute.For<ICollectionEtag>(),
_logger);
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
playout.Templates.ToList(),
null,
[],
[],
TimeSpan.Zero);
Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now,
playout,
referenceData,
PlayoutBuildMode.Reset,
cancellationToken);
buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
}
[Test]
[CancelAfter(10_000)]
public async Task Should_Skip_Block_With_Empty_Collection(CancellationToken cancellationToken)
{
var collection = new SmartCollection
{
Id = 1,
Query = "asdf"
};
var collection2 = new SmartCollection
{
Id = 2,
Query = "asdf2"
};
var block = new Block
{
Id = 1,
Name = "Test Block",
Minutes = 30,
Items =
[
new BlockItem
{
Id = 1,
CollectionType = CollectionType.SmartCollection,
PlaybackOrder = PlaybackOrder.Chronological,
Index = 1,
SmartCollection = collection,
SmartCollectionId = collection.Id
},
new BlockItem
{
Id = 2,
CollectionType = CollectionType.SmartCollection,
PlaybackOrder = PlaybackOrder.Chronological,
Index = 2,
SmartCollection = collection2,
SmartCollectionId = collection2.Id
}
],
StopScheduling = BlockStopScheduling.BeforeDurationEnd
};
var template = new Template
{
Id = 1,
Items = []
};
var templateItem = new TemplateItem
{
Block = block,
BlockId = block.Id,
StartTime = TimeSpan.FromHours(9),
Template = template,
TemplateId = template.Id
};
template.Items.Add(templateItem);
var playoutTemplate = new PlayoutTemplate
{
Id = 1,
Index = 1,
Template = template,
TemplateId = template.Id,
DaysOfMonth = AlternateScheduleSelector.AllDaysOfMonth(),
DaysOfWeek = AlternateScheduleSelector.AllDaysOfWeek(),
MonthsOfYear = AlternateScheduleSelector.AllMonthsOfYear()
};
var playout = new Playout
{
Id = 1,
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
Templates =
[
playoutTemplate
],
Items = [],
PlayoutHistory = []
};
var now = HoursAfterMidnight(9).AddMinutes(15);
var mediaItems = new List<MediaItem>();
var mediaItems2 = new List<MediaItem>
{
new Movie
{
Id = 2,
MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today.AddDays(1) }],
MediaVersions =
[
new MediaVersion
{
Duration = TimeSpan.FromMinutes(25),
MediaFiles = [new MediaFile { Path = "/fake/path/2" }]
}
]
}
};
var collectionRepo = new FakeMediaCollectionRepository(
new Map<int, List<MediaItem>>(
[
(collection.Id, mediaItems),
(collection2.Id, mediaItems2)
])
);
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
configRepo
.GetValue<int>(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any<CancellationToken>())
.Returns(Some(1));
var builder = new BlockPlayoutBuilder(
configRepo,
collectionRepo,
Substitute.For<ITelevisionRepository>(),
Substitute.For<IArtistRepository>(),
Substitute.For<ICollectionEtag>(),
_logger);
var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
playout.Templates.ToList(),
null,
[],
[],
TimeSpan.Zero);
Either<BaseError, PlayoutBuildResult> buildResult = await builder.Build(
now,
playout,
referenceData,
PlayoutBuildMode.Reset,
cancellationToken);
buildResult.IsRight.ShouldBeTrue();
foreach (var result in buildResult.RightToSeq())
{
// this test only cares about "today"
result.AddedItems.RemoveAll(i => i.StartOffset.Date > now.Date);
result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9));
}
}
}
protected static DateTimeOffset HoursAfterMidnight(int hours)
{
DateTimeOffset now = DateTimeOffset.Now;
return now - now.TimeOfDay + TimeSpan.FromHours(hours);
}
}