Browse Source

add mid-roll filler expression (#2138)

pull/2139/head
Jason Dove 1 month ago committed by GitHub
parent
commit
e16cb30ab1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs
  3. 3
      ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs
  4. 3
      ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs
  5. 1
      ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs
  6. 3
      ErsatzTV.Application/Filler/FillerPresetViewModel.cs
  7. 3
      ErsatzTV.Application/Filler/Mapper.cs
  8. 329
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  9. 1
      ErsatzTV.Core/Domain/Filler/FillerPreset.cs
  10. 70
      ErsatzTV.Core/Scheduling/FillerExpression.cs
  11. 54
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  12. 5914
      ErsatzTV.Infrastructure.MySql/Migrations/20250711215753_Add_FillerPresetExpression.Designer.cs
  13. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250711215753_Add_FillerPresetExpression.cs
  14. 5
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  15. 5753
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250711215724_Add_FillerPresetExpression.Designer.cs
  16. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250711215724_Add_FillerPresetExpression.cs
  17. 5
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  18. 28
      ErsatzTV/Pages/FillerPresetEditor.razor
  19. 13
      ErsatzTV/ViewModels/FillerPresetEditViewModel.cs

9
CHANGELOG.md

@ -63,6 +63,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

3
ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs

@ -16,5 +16,6 @@ public record CreateFillerPreset( @@ -16,5 +16,6 @@ public record CreateFillerPreset(
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
int? SmartCollectionId,
string Expression
) : IRequest<Either<BaseError, Unit>>;

3
ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs

@ -36,7 +36,8 @@ public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Eit @@ -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);

3
ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs

@ -17,5 +17,6 @@ public record UpdateFillerPreset( @@ -17,5 +17,6 @@ public record UpdateFillerPreset(
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
int? SmartCollectionId,
string Expression
) : IRequest<Either<BaseError, Unit>>;

1
ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs

@ -37,6 +37,7 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit @@ -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();

3
ErsatzTV.Application/Filler/FillerPresetViewModel.cs

@ -16,4 +16,5 @@ public record FillerPresetViewModel( @@ -16,4 +16,5 @@ public record FillerPresetViewModel(
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId);
int? SmartCollectionId,
string Expression);

3
ErsatzTV.Application/Filler/Mapper.cs

@ -18,5 +18,6 @@ internal static class Mapper @@ -18,5 +18,6 @@ internal static class Mapper
fillerPreset.CollectionId,
fillerPreset.MediaItemId,
fillerPreset.MultiCollectionId,
fillerPreset.SmartCollectionId);
fillerPreset.SmartCollectionId,
fillerPreset.Expression);
}

329
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs

@ -296,6 +296,227 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase @@ -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 @@ -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]

1
ErsatzTV.Core/Domain/Filler/FillerPreset.cs

@ -19,4 +19,5 @@ public class FillerPreset @@ -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; }
}

70
ErsatzTV.Core/Scheduling/FillerExpression.cs

@ -0,0 +1,70 @@ @@ -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;
}
}

54
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -248,7 +248,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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)

5914
ErsatzTV.Infrastructure.MySql/Migrations/20250711215753_Add_FillerPresetExpression.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250711215753_Add_FillerPresetExpression.cs

@ -0,0 +1,29 @@ @@ -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");
}
}
}

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

@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -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 @@ -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");

5753
ErsatzTV.Infrastructure.Sqlite/Migrations/20250711215724_Add_FillerPresetExpression.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250711215724_Add_FillerPresetExpression.cs

@ -0,0 +1,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");
}
}
}

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

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -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 @@ -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");

28
ErsatzTV/Pages/FillerPresetEditor.razor

@ -178,6 +178,33 @@ @@ -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 @@ @@ -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

13
ErsatzTV/ViewModels/FillerPresetEditViewModel.cs

@ -30,6 +30,11 @@ public class FillerPresetEditViewModel @@ -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 @@ -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 @@ -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 @@ -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;

Loading…
Cancel
Save