add mid-roll filler expression (#2138)
This commit is contained in:
@@ -63,6 +63,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- `random % 4 + 1` will play between 1 and 4 items
|
||||
- `2` (similar to before this change) will play exactly two items
|
||||
- Show health check warning and error badges in nav menu
|
||||
- Add `Expression` for mid-roll filler to allow custom logic for using or skipping chapter markers
|
||||
- The following parameters can be used:
|
||||
- `total_points`: total number of potential mid-roll points
|
||||
- `total_duration`: total duration of the content, in seconds
|
||||
- `total_progress`: normalized position from 0 to 1
|
||||
- `last_mid_filler`: seconds since last mid-roll filler
|
||||
- `remaining_duration`: duration of the content after this mid-roll point, in seconds
|
||||
- `point`: the position of the mid-roll point, in seconds
|
||||
- `num`: the mid-roll point number, starting with 1
|
||||
|
||||
### Changed
|
||||
- Allow `Other Video` libraries and `Image` libraries to use the same folders
|
||||
|
||||
@@ -16,5 +16,6 @@ public record CreateFillerPreset(
|
||||
int? CollectionId,
|
||||
int? MediaItemId,
|
||||
int? MultiCollectionId,
|
||||
int? SmartCollectionId
|
||||
int? SmartCollectionId,
|
||||
string Expression
|
||||
) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -36,7 +36,8 @@ public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Eit
|
||||
CollectionId = request.CollectionId,
|
||||
MediaItemId = request.MediaItemId,
|
||||
MultiCollectionId = request.MultiCollectionId,
|
||||
SmartCollectionId = request.SmartCollectionId
|
||||
SmartCollectionId = request.SmartCollectionId,
|
||||
Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null
|
||||
};
|
||||
|
||||
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
|
||||
|
||||
@@ -17,5 +17,6 @@ public record UpdateFillerPreset(
|
||||
int? CollectionId,
|
||||
int? MediaItemId,
|
||||
int? MultiCollectionId,
|
||||
int? SmartCollectionId
|
||||
int? SmartCollectionId,
|
||||
string Expression
|
||||
) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -37,6 +37,7 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
|
||||
existing.MediaItemId = request.MediaItemId;
|
||||
existing.MultiCollectionId = request.MultiCollectionId;
|
||||
existing.SmartCollectionId = request.SmartCollectionId;
|
||||
existing.Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -16,4 +16,5 @@ public record FillerPresetViewModel(
|
||||
int? CollectionId,
|
||||
int? MediaItemId,
|
||||
int? MultiCollectionId,
|
||||
int? SmartCollectionId);
|
||||
int? SmartCollectionId,
|
||||
string Expression);
|
||||
|
||||
@@ -18,5 +18,6 @@ internal static class Mapper
|
||||
fillerPreset.CollectionId,
|
||||
fillerPreset.MediaItemId,
|
||||
fillerPreset.MultiCollectionId,
|
||||
fillerPreset.SmartCollectionId);
|
||||
fillerPreset.SmartCollectionId,
|
||||
fillerPreset.Expression);
|
||||
}
|
||||
|
||||
@@ -296,6 +296,227 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
playoutItems[4].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(55));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Schedule_Post_Roll_After_Padded_Mid_Roll_With_Expression()
|
||||
{
|
||||
// content 45 min, mid roll pad to 60, post roll 5 min
|
||||
// content + post = 50 min, mid roll will add two 5 min items
|
||||
// content + mid + post = 60 min
|
||||
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
MidRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.MidRoll,
|
||||
FillerMode = FillerMode.Pad,
|
||||
PadToNearestMinute = 60,
|
||||
CollectionId = 2,
|
||||
Collection = collectionTwo,
|
||||
Expression = "point > (5 * 60) and total_progress < 0.5"
|
||||
},
|
||||
PostRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.PostRoll,
|
||||
FillerMode = FillerMode.Count,
|
||||
Count = 1,
|
||||
CollectionId = 3,
|
||||
Collection = collectionThree
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var midRollFillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var postRollFillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators = CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator);
|
||||
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), midRollFillerEnumerator);
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.PostRollFiller), postRollFillerEnumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = _scheduler
|
||||
.AddFiller(
|
||||
startState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
new PlayoutItem
|
||||
{
|
||||
MediaItemId = 1,
|
||||
Start = startState.CurrentTime.UtcDateTime,
|
||||
Finish = startState.CurrentTime.AddHours(1).UtcDateTime
|
||||
},
|
||||
[
|
||||
new MediaChapter { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(3) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(3), EndTime = TimeSpan.FromMinutes(6) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(30) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(30), EndTime = TimeSpan.FromMinutes(45) }
|
||||
],
|
||||
true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.ShouldBe(5);
|
||||
|
||||
// content chapter 1
|
||||
playoutItems[0].MediaItemId.ShouldBe(1);
|
||||
playoutItems[0].StartOffset.ShouldBe(startState.CurrentTime);
|
||||
|
||||
// mid-roll 1
|
||||
playoutItems[1].MediaItemId.ShouldBe(3);
|
||||
playoutItems[1].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(6));
|
||||
|
||||
// mid-roll 2
|
||||
playoutItems[2].MediaItemId.ShouldBe(4);
|
||||
playoutItems[2].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(11));
|
||||
|
||||
// content chapter 2
|
||||
playoutItems[3].MediaItemId.ShouldBe(1);
|
||||
playoutItems[3].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(16));
|
||||
|
||||
// post-roll
|
||||
playoutItems[4].MediaItemId.ShouldBe(5);
|
||||
playoutItems[4].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(55));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Schedule_Post_Roll_After_Padded_Mid_Roll_With_Expression_Split()
|
||||
{
|
||||
// content 45 min, mid roll pad to 60, post roll 5 min
|
||||
// content + post = 50 min, mid roll will add two 5 min items
|
||||
// content + mid + post = 60 min
|
||||
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
MidRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.MidRoll,
|
||||
FillerMode = FillerMode.Pad,
|
||||
PadToNearestMinute = 60,
|
||||
CollectionId = 2,
|
||||
Collection = collectionTwo,
|
||||
Expression = "chapter_num % 2 == 0"
|
||||
},
|
||||
PostRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.PostRoll,
|
||||
FillerMode = FillerMode.Count,
|
||||
Count = 1,
|
||||
CollectionId = 3,
|
||||
Collection = collectionThree
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var midRollFillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var postRollFillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators = CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator);
|
||||
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), midRollFillerEnumerator);
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.PostRollFiller), postRollFillerEnumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = _scheduler
|
||||
.AddFiller(
|
||||
startState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
new PlayoutItem
|
||||
{
|
||||
MediaItemId = 1,
|
||||
Start = startState.CurrentTime.UtcDateTime,
|
||||
Finish = startState.CurrentTime.AddHours(1).UtcDateTime
|
||||
},
|
||||
[
|
||||
new MediaChapter { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(3) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(3), EndTime = TimeSpan.FromMinutes(6) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(20) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(20), EndTime = TimeSpan.FromMinutes(30) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(30), EndTime = TimeSpan.FromMinutes(45) }
|
||||
],
|
||||
true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.ShouldBe(6);
|
||||
|
||||
// content chapter 1
|
||||
playoutItems[0].MediaItemId.ShouldBe(1);
|
||||
playoutItems[0].StartOffset.ShouldBe(startState.CurrentTime);
|
||||
|
||||
// mid-roll 1
|
||||
playoutItems[1].MediaItemId.ShouldBe(3);
|
||||
playoutItems[1].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(6));
|
||||
|
||||
// content chapter 2
|
||||
playoutItems[2].MediaItemId.ShouldBe(1);
|
||||
playoutItems[2].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(11));
|
||||
|
||||
// mid-roll 2
|
||||
playoutItems[3].MediaItemId.ShouldBe(4);
|
||||
playoutItems[3].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(35));
|
||||
|
||||
// content chapter 3
|
||||
playoutItems[4].MediaItemId.ShouldBe(1);
|
||||
playoutItems[4].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(40));
|
||||
|
||||
// post-roll
|
||||
playoutItems[5].MediaItemId.ShouldBe(5);
|
||||
playoutItems[5].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(55));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Schedule_Padded_Post_Roll_After_Mid_Roll_Count()
|
||||
{
|
||||
@@ -401,6 +622,114 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
playoutItems[4].MediaItemId.ShouldBe(6);
|
||||
playoutItems[4].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(55));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Schedule_Padded_Post_Roll_After_Mid_Roll_Count_With_Expression()
|
||||
{
|
||||
// content 45 min, mid roll 5 min, post roll pad to 60
|
||||
// content + mid = 50 min, post roll will add two 5 min items
|
||||
// content + mid + post = 60 min
|
||||
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
MidRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.MidRoll,
|
||||
FillerMode = FillerMode.Count,
|
||||
Count = 1,
|
||||
CollectionId = 2,
|
||||
Collection = collectionTwo,
|
||||
Expression = "point > (5 * 60) and total_progress < 0.5"
|
||||
},
|
||||
PostRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.PostRoll,
|
||||
FillerMode = FillerMode.Pad,
|
||||
PadToNearestMinute = 60,
|
||||
CollectionId = 3,
|
||||
Collection = collectionThree
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var midRollFillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var postRollFillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators = CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator);
|
||||
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), midRollFillerEnumerator);
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.PostRollFiller), postRollFillerEnumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = _scheduler
|
||||
.AddFiller(
|
||||
startState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
new PlayoutItem
|
||||
{
|
||||
MediaItemId = 1,
|
||||
Start = startState.CurrentTime.UtcDateTime,
|
||||
Finish = startState.CurrentTime.AddHours(1).UtcDateTime
|
||||
},
|
||||
[
|
||||
new MediaChapter { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(3) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(3), EndTime = TimeSpan.FromMinutes(6) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(30) },
|
||||
new MediaChapter { StartTime = TimeSpan.FromMinutes(30), EndTime = TimeSpan.FromMinutes(45) }
|
||||
],
|
||||
true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.ShouldBe(5);
|
||||
|
||||
// content chapter 1
|
||||
playoutItems[0].MediaItemId.ShouldBe(1);
|
||||
playoutItems[0].StartOffset.ShouldBe(startState.CurrentTime);
|
||||
|
||||
// mid-roll 1
|
||||
playoutItems[1].MediaItemId.ShouldBe(3);
|
||||
playoutItems[1].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(6));
|
||||
|
||||
// content chapter 2
|
||||
playoutItems[2].MediaItemId.ShouldBe(1);
|
||||
playoutItems[2].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(11));
|
||||
|
||||
// post-roll 1
|
||||
playoutItems[3].MediaItemId.ShouldBe(5);
|
||||
playoutItems[3].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(50));
|
||||
|
||||
// post-roll 2
|
||||
playoutItems[4].MediaItemId.ShouldBe(6);
|
||||
playoutItems[4].StartOffset.ShouldBe(startState.CurrentTime + TimeSpan.FromMinutes(55));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
|
||||
@@ -19,4 +19,5 @@ public class FillerPreset
|
||||
public MultiCollection MultiCollection { get; set; }
|
||||
public int? SmartCollectionId { get; set; }
|
||||
public SmartCollection SmartCollection { get; set; }
|
||||
public string Expression { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using NCalc;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
public static class FillerExpression
|
||||
{
|
||||
public static List<MediaChapter> FilterChapters(FillerPreset fillerPreset, List<MediaChapter> effectiveChapters, PlayoutItem playoutItem)
|
||||
{
|
||||
if (effectiveChapters.Count == 0 || fillerPreset is null || string.IsNullOrWhiteSpace(fillerPreset.Expression))
|
||||
{
|
||||
return effectiveChapters;
|
||||
}
|
||||
|
||||
var chapterPoints = effectiveChapters.Map(c => c.EndTime).SkipLast().ToList();
|
||||
|
||||
var newChapters = new List<MediaChapter>();
|
||||
|
||||
TimeSpan start = effectiveChapters.Select(c => c.StartTime).Min();
|
||||
TimeSpan end = effectiveChapters.Select(c => c.EndTime).Max();
|
||||
|
||||
double lastFiller = start.TotalSeconds - 99999.0;
|
||||
double contentDuration = (playoutItem.FinishOffset - playoutItem.StartOffset).TotalSeconds;
|
||||
|
||||
for (var index = 0; index < chapterPoints.Count; index++)
|
||||
{
|
||||
TimeSpan chapterPoint = chapterPoints[index];
|
||||
var expression = new Expression(fillerPreset.Expression);
|
||||
int chapterNum = index + 1;
|
||||
double sinceLastFiller = chapterPoint.TotalSeconds - lastFiller;
|
||||
expression.EvaluateParameter += (name, e) =>
|
||||
{
|
||||
e.Result = name switch
|
||||
{
|
||||
"last_mid_filler" => sinceLastFiller,
|
||||
"total_points" => effectiveChapters.Count - 1,
|
||||
"total_duration" => contentDuration,
|
||||
"total_progress" => chapterPoint.TotalSeconds / end.TotalSeconds,
|
||||
"remaining_duration" => contentDuration - chapterPoint.TotalSeconds,
|
||||
"point" => chapterPoint.TotalSeconds,
|
||||
"num" => chapterNum,
|
||||
_ => e.Result
|
||||
};
|
||||
};
|
||||
|
||||
if (expression.Evaluate() as bool? == true)
|
||||
{
|
||||
lastFiller = chapterPoint.TotalSeconds;
|
||||
newChapters.Add(effectiveChapters[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (newChapters.Count > 0)
|
||||
{
|
||||
newChapters[0].StartTime = start;
|
||||
|
||||
TimeSpan currentTime = start;
|
||||
foreach (MediaChapter chapter in newChapters)
|
||||
{
|
||||
chapter.StartTime = currentTime;
|
||||
currentTime = chapter.EndTime;
|
||||
}
|
||||
|
||||
newChapters.Add(new MediaChapter { StartTime = currentTime, EndTime = end });
|
||||
}
|
||||
|
||||
return newChapters;
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1)
|
||||
{
|
||||
Logger.LogError("Multiple pad-to-nearest-minute values are invalid; no filler will be used");
|
||||
return new List<PlayoutItem> { playoutItem };
|
||||
return [playoutItem];
|
||||
}
|
||||
|
||||
// missing pad-to-nearest-minute value is invalid; use no filler
|
||||
@@ -259,7 +259,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
Logger.LogError(
|
||||
"Pad filler ({Filler}) without pad-to-nearest-minute value is invalid; no filler will be used",
|
||||
invalidPadFiller.Name);
|
||||
return new List<PlayoutItem> { playoutItem };
|
||||
return [playoutItem];
|
||||
}
|
||||
|
||||
List<MediaChapter> effectiveChapters = chapters;
|
||||
@@ -353,14 +353,21 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
foreach (FillerPreset filler in allFiller.Filter(f =>
|
||||
f.FillerKind == FillerKind.MidRoll && f.FillerMode != FillerMode.Pad))
|
||||
{
|
||||
List<MediaChapter> filteredChapters = FillerExpression.FilterChapters(filler, effectiveChapters, playoutItem);
|
||||
if (filteredChapters.Count <= 1)
|
||||
{
|
||||
result.Add(playoutItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (filler.FillerMode)
|
||||
{
|
||||
case FillerMode.Duration when filler.Duration.HasValue:
|
||||
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
for (var i = 0; i < filteredChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
result.Add(playoutItem.ForChapter(filteredChapters[i]));
|
||||
if (i < filteredChapters.Count - 1)
|
||||
{
|
||||
result.AddRange(
|
||||
AddDurationFiller(
|
||||
@@ -379,10 +386,10 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
for (var i = 0; i < filteredChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
result.Add(playoutItem.ForChapter(filteredChapters[i]));
|
||||
if (i < filteredChapters.Count - 1)
|
||||
{
|
||||
result.AddRange(
|
||||
AddCountFiller(
|
||||
@@ -400,10 +407,10 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
break;
|
||||
case FillerMode.RandomCount when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e3 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
for (var i = 0; i < filteredChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
result.Add(playoutItem.ForChapter(filteredChapters[i]));
|
||||
if (i < filteredChapters.Count - 1)
|
||||
{
|
||||
result.AddRange(
|
||||
AddRandomCountFiller(
|
||||
@@ -471,10 +478,19 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
{
|
||||
var totalDuration = TimeSpan.FromTicks(result.Sum(pi => (pi.Finish - pi.Start).Ticks));
|
||||
|
||||
List<MediaChapter> filteredChapters =
|
||||
FillerExpression.FilterChapters(padFiller, effectiveChapters, playoutItem);
|
||||
|
||||
FillerKind fillerKind = padFiller.FillerKind;
|
||||
if (filteredChapters.Count <= 1 && effectiveChapters.Count > 1)
|
||||
{
|
||||
fillerKind = FillerKind.PostRoll;
|
||||
}
|
||||
|
||||
// add primary content to totalDuration only if it hasn't already been added
|
||||
if (result.All(pi => pi.MediaItemId != playoutItem.MediaItemId))
|
||||
{
|
||||
totalDuration += TimeSpan.FromTicks(effectiveChapters.Sum(c => (c.EndTime - c.StartTime).Ticks));
|
||||
totalDuration += TimeSpan.FromTicks(filteredChapters.Sum(c => (c.EndTime - c.StartTime).Ticks));
|
||||
}
|
||||
|
||||
int currentMinute = (playoutItem.StartOffset + totalDuration).Minute;
|
||||
@@ -510,7 +526,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
// playoutItem.StartOffset,
|
||||
// targetTime);
|
||||
|
||||
switch (padFiller.FillerKind)
|
||||
switch (fillerKind)
|
||||
{
|
||||
case FillerKind.PreRoll:
|
||||
IMediaCollectionEnumerator pre1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
|
||||
@@ -550,19 +566,19 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
padFiller.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
TimeSpan average = effectiveChapters.Count <= 1
|
||||
TimeSpan average = filteredChapters.Count <= 1
|
||||
? remainingToFill
|
||||
: remainingToFill / (effectiveChapters.Count - 1);
|
||||
: remainingToFill / (filteredChapters.Count - 1);
|
||||
TimeSpan filled = TimeSpan.Zero;
|
||||
|
||||
// remove post-roll to add after mid-roll/content
|
||||
var postRoll = result.Where(i => i.FillerKind == FillerKind.PostRoll).ToList();
|
||||
result.RemoveAll(i => i.FillerKind == FillerKind.PostRoll);
|
||||
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
for (var i = 0; i < filteredChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
result.Add(playoutItem.ForChapter(filteredChapters[i]));
|
||||
if (i < filteredChapters.Count - 1)
|
||||
{
|
||||
TimeSpan current = TimeSpan.Zero;
|
||||
while (current < average && filled < remainingToFill)
|
||||
@@ -586,7 +602,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
playoutBuilderState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
i < effectiveChapters.Count - 1 ? maxThisBreak : leftOverall,
|
||||
i < filteredChapters.Count - 1 ? maxThisBreak : leftOverall,
|
||||
cancellationToken);
|
||||
|
||||
foreach (PlayoutItem fallback in maybeFallback)
|
||||
|
||||
Generated
+5914
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_FillerPresetExpression : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Expression",
|
||||
table: "FillerPreset",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Expression",
|
||||
table: "FillerPreset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
@@ -709,6 +709,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<TimeSpan?>("Duration")
|
||||
.HasColumnType("time(6)");
|
||||
|
||||
b.Property<string>("Expression")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("FillerKind")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
||||
Generated
+5753
File diff suppressed because it is too large
Load Diff
+28
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_FillerPresetExpression : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Expression",
|
||||
table: "FillerPreset",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Expression",
|
||||
table: "FillerPreset");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
|
||||
{
|
||||
@@ -676,6 +676,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<TimeSpan?>("Duration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Expression")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("FillerKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -178,6 +178,33 @@
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
}
|
||||
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
|
||||
<div class="d-flex">
|
||||
<MudText>Expression</MudText>
|
||||
</div>
|
||||
<MudTextField @bind-Value="_model.Expression"
|
||||
For="@(() => _model.Expression)"
|
||||
Disabled="@(_model.FillerKind is not FillerKind.MidRoll)"
|
||||
HelperText="For mid-roll filler, only add filler when this expression evaluates to true for a given mid-roll point."/>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="gap-md-8 mb-5">
|
||||
<div class="d-flex" style="width: 300px"></div>
|
||||
<MudText Typo="Typo.body2">
|
||||
<span style="font-weight: bold;">total_points</span>: total number of potential mid-roll points
|
||||
<br />
|
||||
<span style="font-weight: bold;">total_duration</span>: total duration of the content, in seconds
|
||||
<br />
|
||||
<span style="font-weight: bold;">total_progress</span>: normalized position from 0 to 1
|
||||
<br />
|
||||
<span style="font-weight: bold;">last_mid_filler</span>: seconds since last mid-roll filler
|
||||
<br />
|
||||
<span style="font-weight: bold;">remaining_duration</span>: duration of the content after this mid-roll point, in seconds
|
||||
<br />
|
||||
<span style="font-weight: bold;">point</span>: the position of the mid-roll point, in seconds
|
||||
<br />
|
||||
<span style="font-weight: bold;">num</span>: the mid-roll point number, starting with 1
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
</div>
|
||||
</MudForm>
|
||||
@@ -246,6 +273,7 @@
|
||||
_model.MediaItem = fillerPreset.MediaItemId.HasValue
|
||||
? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList().Find(vm => vm.MediaItemId == fillerPreset.MediaItemId.Value)
|
||||
: null;
|
||||
_model.Expression = fillerPreset.Expression;
|
||||
});
|
||||
}
|
||||
else
|
||||
|
||||
@@ -30,6 +30,11 @@ public class FillerPresetEditViewModel
|
||||
{
|
||||
FillerMode = FillerMode.None;
|
||||
}
|
||||
|
||||
if (_fillerKind is not FillerKind.MidRoll)
|
||||
{
|
||||
Expression = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +86,8 @@ public class FillerPresetEditViewModel
|
||||
public MultiCollectionViewModel MultiCollection { get; set; }
|
||||
public SmartCollectionViewModel SmartCollection { get; set; }
|
||||
|
||||
public string Expression { get; set; }
|
||||
|
||||
public IRequest<Either<BaseError, Unit>> ToEdit() =>
|
||||
new UpdateFillerPreset(
|
||||
Id,
|
||||
@@ -95,7 +102,8 @@ public class FillerPresetEditViewModel
|
||||
Collection?.Id,
|
||||
MediaItem?.MediaItemId,
|
||||
MultiCollection?.Id,
|
||||
SmartCollection?.Id);
|
||||
SmartCollection?.Id,
|
||||
Expression);
|
||||
|
||||
public IRequest<Either<BaseError, Unit>> ToUpdate() =>
|
||||
new CreateFillerPreset(
|
||||
@@ -110,7 +118,8 @@ public class FillerPresetEditViewModel
|
||||
Collection?.Id,
|
||||
MediaItem?.MediaItemId,
|
||||
MultiCollection?.Id,
|
||||
SmartCollection?.Id);
|
||||
SmartCollection?.Id,
|
||||
Expression);
|
||||
|
||||
private static TimeSpan FixDuration(TimeSpan duration) =>
|
||||
duration > TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
|
||||
|
||||
Reference in New Issue
Block a user