block schedules: skip items and collections that will never fit (#2433)
* add first block playout builder test * block schedules: skip items and collections that will never fit
This commit is contained in:
@@ -44,6 +44,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- This can happen when NVIDIA accel falls back to libx264 software encoder for 10-bit h264 output
|
||||
- Fix 10-bit output when using NVIDIA and graphics engine (watermark or other overlays)
|
||||
- Fix playback of Jellyfin content with unknown color range
|
||||
- Block schedules: skip collections (block items) that will never fit in block duration
|
||||
- Block schedules: skip media items that will never fit in block duration
|
||||
|
||||
## [25.6.0] - 2025-09-14
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
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 = PlayoutTemplate.AllDaysOfMonth(),
|
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
|
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear()
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
Id = 1,
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Templates =
|
||||
[
|
||||
playoutTemplate
|
||||
],
|
||||
Items = [],
|
||||
PlayoutHistory = []
|
||||
};
|
||||
|
||||
var now = new DateTimeOffset(2024, 1, 10, 9, 15, 0, TimeSpan.FromHours(-6));
|
||||
|
||||
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);
|
||||
|
||||
PlayoutBuildResult result = await builder.Build(
|
||||
now,
|
||||
playout,
|
||||
referenceData,
|
||||
PlayoutBuildMode.Reset,
|
||||
cancellationToken);
|
||||
|
||||
// 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 = PlayoutTemplate.AllDaysOfMonth(),
|
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
|
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear()
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
Id = 1,
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Templates =
|
||||
[
|
||||
playoutTemplate
|
||||
],
|
||||
Items = [],
|
||||
PlayoutHistory = []
|
||||
};
|
||||
|
||||
var now = new DateTimeOffset(2024, 1, 10, 9, 15, 0, TimeSpan.FromHours(-6));
|
||||
|
||||
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);
|
||||
|
||||
PlayoutBuildResult result = await builder.Build(
|
||||
now,
|
||||
playout,
|
||||
referenceData,
|
||||
PlayoutBuildMode.Reset,
|
||||
cancellationToken);
|
||||
|
||||
// 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 = PlayoutTemplate.AllDaysOfMonth(),
|
||||
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
|
||||
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear()
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
Id = 1,
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Templates =
|
||||
[
|
||||
playoutTemplate
|
||||
],
|
||||
Items = [],
|
||||
PlayoutHistory = []
|
||||
};
|
||||
|
||||
var now = new DateTimeOffset(2024, 1, 10, 9, 15, 0, TimeSpan.FromHours(-6));
|
||||
|
||||
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);
|
||||
|
||||
PlayoutBuildResult result = await builder.Build(
|
||||
now,
|
||||
playout,
|
||||
referenceData,
|
||||
PlayoutBuildMode.Reset,
|
||||
cancellationToken);
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,99 +173,138 @@ public class BlockPlayoutBuilder(
|
||||
collectionMediaItems);
|
||||
|
||||
var pastTime = false;
|
||||
var done = false;
|
||||
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
while (!done && !pastTime)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"current item: {Id} / {Title}",
|
||||
mediaItem.Id,
|
||||
mediaItem is Episode e ? GetTitle(e) : string.Empty);
|
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
|
||||
var collectionKey = CollectionKey.ForBlockItem(blockItem);
|
||||
|
||||
// create a playout item
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
PlayoutId = playout.Id,
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = currentTime.UtcDateTime,
|
||||
Finish = currentTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
FillerKind = blockItem.IncludeInProgramGuide ? FillerKind.None : FillerKind.GuideMode,
|
||||
DisableWatermarks = blockItem.DisableWatermarks,
|
||||
//CustomTitle = scheduleItem.CustomTitle,
|
||||
//WatermarkId = scheduleItem.WatermarkId,
|
||||
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
//SubtitleMode = scheduleItem.SubtitleMode
|
||||
GuideGroup = effectiveBlock.TemplateItemId,
|
||||
GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||
GuideFinish = blockFinish.UtcDateTime,
|
||||
BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||
CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
||||
CollectionEtag = collectionEtags[collectionKey],
|
||||
PlayoutItemWatermarks = [],
|
||||
PlayoutItemGraphicsElements = []
|
||||
};
|
||||
|
||||
foreach (BlockItemWatermark blockItemWatermark in blockItem.BlockItemWatermarks ?? [])
|
||||
{
|
||||
playoutItem.PlayoutItemWatermarks.Add(
|
||||
new PlayoutItemWatermark
|
||||
{
|
||||
PlayoutItem = playoutItem,
|
||||
WatermarkId = blockItemWatermark.WatermarkId
|
||||
});
|
||||
}
|
||||
|
||||
foreach (BlockItemGraphicsElement blockItemGraphicsElement in blockItem.BlockItemGraphicsElements ??
|
||||
[])
|
||||
{
|
||||
playoutItem.PlayoutItemGraphicsElements.Add(
|
||||
new PlayoutItemGraphicsElement
|
||||
{
|
||||
PlayoutItem = playoutItem,
|
||||
GraphicsElementId = blockItemGraphicsElement.GraphicsElementId
|
||||
});
|
||||
}
|
||||
|
||||
if (effectiveBlock.Block.StopScheduling is BlockStopScheduling.BeforeDurationEnd
|
||||
&& playoutItem.FinishOffset > blockFinish)
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Current time {Time} for block {Block} would go beyond block finish {Finish}; will not schedule more items",
|
||||
currentTime,
|
||||
effectiveBlock.Block.Name,
|
||||
blockFinish);
|
||||
"current item: {Id} / {Title}",
|
||||
mediaItem.Id,
|
||||
PlayoutBuilder.DisplayTitle(mediaItem));
|
||||
|
||||
pastTime = true;
|
||||
break;
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
|
||||
// item will never fit in block
|
||||
var blockDuration = TimeSpan.FromMinutes(effectiveBlock.Block.Minutes);
|
||||
if (effectiveBlock.Block.StopScheduling is BlockStopScheduling.BeforeDurationEnd &&
|
||||
itemDuration > blockDuration)
|
||||
{
|
||||
foreach (TimeSpan minimumDuration in enumerator.MinimumDuration)
|
||||
{
|
||||
if (minimumDuration > blockDuration)
|
||||
{
|
||||
Logger.LogError(
|
||||
"Collection with minimum duration {Duration:hh\\:mm\\:ss} will never fit in block with duration {BlockDuration:hh\\:mm\\:ss}; skipping this block item!",
|
||||
minimumDuration,
|
||||
blockDuration);
|
||||
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (done)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.LogWarning(
|
||||
"Skipping playout item {Title} with duration {Duration:hh\\:mm\\:ss} that will never fit in block with duration {BlockDuration:hh\\:mm\\:ss}",
|
||||
PlayoutBuilder.DisplayTitle(mediaItem),
|
||||
itemDuration,
|
||||
blockDuration);
|
||||
|
||||
enumerator.MoveNext(Option<DateTimeOffset>.None);
|
||||
continue;
|
||||
}
|
||||
|
||||
var collectionKey = CollectionKey.ForBlockItem(blockItem);
|
||||
|
||||
// create a playout item
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
PlayoutId = playout.Id,
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = currentTime.UtcDateTime,
|
||||
Finish = currentTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
FillerKind = blockItem.IncludeInProgramGuide ? FillerKind.None : FillerKind.GuideMode,
|
||||
DisableWatermarks = blockItem.DisableWatermarks,
|
||||
//CustomTitle = scheduleItem.CustomTitle,
|
||||
//WatermarkId = scheduleItem.WatermarkId,
|
||||
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
//SubtitleMode = scheduleItem.SubtitleMode
|
||||
GuideGroup = effectiveBlock.TemplateItemId,
|
||||
GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||
GuideFinish = blockFinish.UtcDateTime,
|
||||
BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||
CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
||||
CollectionEtag = collectionEtags[collectionKey],
|
||||
PlayoutItemWatermarks = [],
|
||||
PlayoutItemGraphicsElements = []
|
||||
};
|
||||
|
||||
foreach (BlockItemWatermark blockItemWatermark in blockItem.BlockItemWatermarks ?? [])
|
||||
{
|
||||
playoutItem.PlayoutItemWatermarks.Add(
|
||||
new PlayoutItemWatermark
|
||||
{
|
||||
PlayoutItem = playoutItem,
|
||||
WatermarkId = blockItemWatermark.WatermarkId
|
||||
});
|
||||
}
|
||||
|
||||
foreach (BlockItemGraphicsElement blockItemGraphicsElement in blockItem
|
||||
.BlockItemGraphicsElements ??
|
||||
[])
|
||||
{
|
||||
playoutItem.PlayoutItemGraphicsElements.Add(
|
||||
new PlayoutItemGraphicsElement
|
||||
{
|
||||
PlayoutItem = playoutItem,
|
||||
GraphicsElementId = blockItemGraphicsElement.GraphicsElementId
|
||||
});
|
||||
}
|
||||
|
||||
if (effectiveBlock.Block.StopScheduling is BlockStopScheduling.BeforeDurationEnd
|
||||
&& playoutItem.FinishOffset > blockFinish)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Current time {Time} for block {Block} would go beyond block finish {Finish}; will not schedule more items",
|
||||
currentTime,
|
||||
effectiveBlock.Block.Name,
|
||||
blockFinish);
|
||||
|
||||
pastTime = true;
|
||||
break;
|
||||
}
|
||||
|
||||
result.AddedItems.Add(playoutItem);
|
||||
|
||||
// create a playout history record
|
||||
var nextHistory = new PlayoutHistory
|
||||
{
|
||||
PlayoutId = playout.Id,
|
||||
BlockId = blockItem.BlockId,
|
||||
PlaybackOrder = blockItem.PlaybackOrder,
|
||||
Index = enumerator.State.Index,
|
||||
When = currentTime.UtcDateTime,
|
||||
Finish = playoutItem.FinishOffset.UtcDateTime,
|
||||
Key = historyKey,
|
||||
Details = HistoryDetails.ForMediaItem(mediaItem)
|
||||
};
|
||||
|
||||
//logger.LogDebug("Adding history item: {When}: {History}", nextHistory.When, nextHistory.Details);
|
||||
result.AddedHistory.Add(nextHistory);
|
||||
|
||||
currentTime += itemDuration;
|
||||
enumerator.MoveNext(playoutItem.StartOffset);
|
||||
done = true;
|
||||
}
|
||||
|
||||
result.AddedItems.Add(playoutItem);
|
||||
|
||||
// create a playout history record
|
||||
var nextHistory = new PlayoutHistory
|
||||
{
|
||||
PlayoutId = playout.Id,
|
||||
BlockId = blockItem.BlockId,
|
||||
PlaybackOrder = blockItem.PlaybackOrder,
|
||||
Index = enumerator.State.Index,
|
||||
When = currentTime.UtcDateTime,
|
||||
Finish = playoutItem.FinishOffset.UtcDateTime,
|
||||
Key = historyKey,
|
||||
Details = HistoryDetails.ForMediaItem(mediaItem)
|
||||
};
|
||||
|
||||
//logger.LogDebug("Adding history item: {When}: {History}", nextHistory.When, nextHistory.Details);
|
||||
result.AddedHistory.Add(nextHistory);
|
||||
|
||||
currentTime += itemDuration;
|
||||
enumerator.MoveNext(playoutItem.StartOffset);
|
||||
}
|
||||
|
||||
if (pastTime)
|
||||
@@ -336,23 +375,6 @@ public class BlockPlayoutBuilder(
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
private static string GetTitle(Episode e)
|
||||
{
|
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
|
||||
var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList();
|
||||
var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList();
|
||||
if (episodeNumbers.Count == 0 || episodeTitles.Count == 0)
|
||||
{
|
||||
return "[unknown episode]";
|
||||
}
|
||||
|
||||
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
|
||||
var titlesString = $"{string.Join('/', episodeTitles)}";
|
||||
|
||||
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
|
||||
}
|
||||
|
||||
private static PlayoutBuildResult CleanUpHistory(
|
||||
PlayoutReferenceData referenceData,
|
||||
DateTimeOffset start,
|
||||
|
||||
Reference in New Issue
Block a user