make count an expression in classic schedules (#2794)

* make count an expression in classic schedules

* add tests
This commit is contained in:
Jason Dove
2026-01-20 09:50:45 -06:00
committed by GitHub
parent 3d81f760ee
commit 08ceb53b2b
33 changed files with 14167 additions and 74 deletions

View File

@@ -22,6 +22,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Disable automatic artwork database cleanup
- This will be re-enabled at some point in the future (after more testing)
- For now, the API should be used to clean as needed
- Classic Schedules: make multiple `count` an expression
- The following parameters can be used:
- `count`: the total number of items in the collection
- `random`: a random number between zero and (count - 1)
- For example:
- `count / 2` will play half of the items in the collection
- `random % 4 + 1` will play between 1 and 4 items
- `2` (similar to before this change) will play exactly two items
### Fixed
- Use code signing on all Windows executables (`ErsatzTV-Windows.exe`, `ErsatzTV.exe`, `ErsatzTV.Scanner.exe`)
@@ -760,13 +768,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `random` will start at a random point in the content
- `2` (similar to before this change) will skip the first two items in the content
- YAML playout: make `count` an expression
- The following parameters can be used:
- `count`: the total number of items in the content
- `random`: a random number between zero and (count - 1)
- For example:
- `count / 2` will play half of the items in the content
- `random % 4 + 1` will play between 1 and 4 items
- `2` (similar to before this change) will play exactly two items
- The following parameters can be used:
- `count`: the total number of items in the content
- `random`: a random number between zero and (count - 1)
- For example:
- `count / 2` will play half of the items in the content
- `random % 4 + 1` will play between 1 and 4 items
- `2` (similar to before this change) will play exactly two items
- YAML playout: add `disable_watermarks` property to all content instructions
- This property defaults to `false` (meaning watermarks are allowed by default)
- Setting to `true` will prevent watermarks from ever appearing over the content

View File

@@ -26,7 +26,7 @@ public record AddProgramScheduleItem(
int? MarathonBatchSize,
FillWithGroupMode FillWithGroupMode,
MultipleMode MultipleMode,
int? MultipleCount,
string MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,

View File

@@ -24,7 +24,7 @@ public interface IProgramScheduleItemRequest
int? MarathonBatchSize { get; }
FillWithGroupMode FillWithGroupMode { get; }
MultipleMode MultipleMode { get; }
int? MultipleCount { get; }
string MultipleCount { get; }
TimeSpan? PlayoutDuration { get; }
TailMode TailMode { get; }
int? DiscardToFillAttempts { get; }

View File

@@ -79,10 +79,10 @@ public abstract class ProgramScheduleItemCommandBase
"[MultipleMode] cannot be [PlaylistItemSize] when collection is not a playlist");
}
if (item.MultipleMode is MultipleMode.Count && item.MultipleCount.GetValueOrDefault() < 1)
if (item.MultipleMode is MultipleMode.Count && string.IsNullOrWhiteSpace(item.MultipleCount))
{
return BaseError.New(
"[MultipleCount] must be greater than 0 for playout mode 'multiple / count'");
"[MultipleCount] must be valid for playout mode 'multiple / count'");
}
break;
@@ -298,7 +298,7 @@ public abstract class ProgramScheduleItemCommandBase
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = item.FillWithGroupMode,
MultipleMode = item.MultipleMode,
Count = item.MultipleMode is MultipleMode.Count ? item.MultipleCount.GetValueOrDefault() : 0,
Count = item.MultipleMode is MultipleMode.Count ? item.MultipleCount ?? "0" : "0",
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId,

View File

@@ -26,7 +26,7 @@ public record ReplaceProgramScheduleItem(
int? MarathonBatchSize,
FillWithGroupMode FillWithGroupMode,
MultipleMode MultipleMode,
int? MultipleCount,
string MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,

View File

@@ -32,7 +32,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
int? marathonBatchSize,
FillWithGroupMode fillWithGroupMode,
MultipleMode multipleMode,
int count,
string count,
string customTitle,
GuideMode guideMode,
FillerPresetViewModel preRollFiller,
@@ -87,5 +87,5 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
public MultipleMode MultipleMode { get; set; }
public int Count { get; }
public string Count { get; }
}

View File

@@ -666,7 +666,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
Count = 3,
Count = "3",
PlaybackOrder = PlaybackOrder.Chronological
},
new ProgramScheduleItemMultiple
@@ -676,7 +676,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
Collection = collectionTwo,
CollectionId = collectionTwo.Id,
StartTime = null,
Count = 3,
Count = "3",
PlaybackOrder = PlaybackOrder.Chronological
}
};

View File

@@ -852,7 +852,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
StartTime = TimeSpan.FromHours(3),
Count = 2,
Count = "2",
PlaybackOrder = PlaybackOrder.Chronological
}
};
@@ -981,7 +981,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
StartTime = TimeSpan.FromHours(3),
Count = 2,
Count = "2",
PlaybackOrder = PlaybackOrder.Chronological
}
};
@@ -1361,7 +1361,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
Collection = multipleCollection,
CollectionId = multipleCollection.Id,
StartTime = null,
Count = 2,
Count = "2",
PlaybackOrder = PlaybackOrder.Chronological
},
new ProgramScheduleItemDuration
@@ -1494,7 +1494,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
Count = 0,
Count = "0",
MultipleMode = MultipleMode.CollectionSize,
PlaybackOrder = PlaybackOrder.Chronological
},
@@ -1505,7 +1505,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
Collection = collectionTwo,
CollectionId = collectionTwo.Id,
StartTime = null,
Count = 0,
Count = "0",
MultipleMode = MultipleMode.CollectionSize,
PlaybackOrder = PlaybackOrder.Chronological
}
@@ -1777,7 +1777,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
Count = 1,
Count = "1",
PlaybackOrder = PlaybackOrder.Chronological,
PostRollFiller = new FillerPreset
{

View File

@@ -0,0 +1,34 @@
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class CountExpressionTests
{
[Test]
[TestCase("2", 2)]
[TestCase("count", 10)]
[TestCase("count / 2", 5)]
[TestCase("count * 2", 20)]
[TestCase("count + 1", 11)]
[TestCase("count - 1", 9)]
[TestCase("random % 4 + 1", 3)]
[TestCase("invalid", 0)]
[TestCase("count / 0", 0)]
public void Should_Evaluate_Expression(string expression, int expected)
{
var enumerator = Substitute.For<IMediaCollectionEnumerator>();
enumerator.Count.Returns(10);
var random = Substitute.For<Random>();
random.Next().Returns(2);
int result = CountExpression.Evaluate(expression, enumerator, random, CancellationToken.None);
result.ShouldBe(expected);
}
}

View File

@@ -796,6 +796,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
ProgramScheduleItem scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken) =>
throw new NotSupportedException();
}

View File

@@ -13,9 +13,14 @@ namespace ErsatzTV.Core.Tests.Scheduling;
public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
_random = new Random();
}
private CancellationToken _cancellationToken;
private Random _random;
private readonly ILogger<PlayoutModeSchedulerDuration> _logger;
public PlayoutModeSchedulerDurationTests()
@@ -66,6 +71,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -139,6 +145,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -211,6 +218,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
@@ -280,6 +288,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
// duration block should end after exact duration, with gap
@@ -363,6 +372,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -450,6 +460,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -549,6 +560,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -665,6 +677,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -816,6 +829,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddMinutes(30));
@@ -880,6 +894,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutItems.ShouldBeEmpty();

View File

@@ -12,9 +12,14 @@ namespace ErsatzTV.Core.Tests.Scheduling;
public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
_random = new Random();
}
private CancellationToken _cancellationToken;
private Random _random;
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
@@ -57,6 +62,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -132,6 +138,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
scheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(6));
@@ -229,6 +236,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -314,6 +322,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -402,6 +411,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
@@ -484,6 +494,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -582,6 +593,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -670,6 +682,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
@@ -784,6 +797,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -896,6 +910,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
hardStop,
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(2));
@@ -1002,6 +1017,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
hardStop,
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(2));
@@ -1116,6 +1132,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -1190,6 +1207,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutItems.ShouldBeEmpty();

View File

@@ -12,9 +12,14 @@ namespace ErsatzTV.Core.Tests.Scheduling;
public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
_random = new Random();
}
private CancellationToken _cancellationToken;
private Random _random;
[Test]
public void Should_Respect_Fixed_Start_Time()
@@ -32,7 +37,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 0,
Count = "0",
MultipleMode = MultipleMode.CollectionSize,
CustomTitle = "CustomTitle"
};
@@ -59,6 +64,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -134,7 +140,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 0,
Count = "0",
MultipleMode = MultipleMode.MultiEpisodeGroupSize,
CustomTitle = "CustomTitle"
};
@@ -161,6 +167,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -206,7 +213,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 3,
Count = "3",
CustomTitle = "CustomTitle"
};
@@ -232,6 +239,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -282,7 +290,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 3
Count = "3"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
@@ -307,6 +315,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
@@ -360,7 +369,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionId = collectionTwo.Id
},
FallbackFiller = null,
Count = 3
Count = "3"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
@@ -390,6 +399,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -459,7 +469,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
Count = 3
Count = "3"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
@@ -489,6 +499,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -548,7 +559,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionId = collectionTwo.Id
},
FallbackFiller = null,
Count = 3
Count = "3"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
@@ -578,6 +589,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
@@ -653,7 +665,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
Collection = collectionThree,
CollectionId = collectionThree.Id
},
Count = 3
Count = "3"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
@@ -694,6 +706,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -775,7 +788,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
Collection = collectionThree,
CollectionId = collectionThree.Id
},
Count = 3
Count = "3"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
@@ -816,6 +829,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -865,7 +879,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 2
Count = "2"
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
@@ -896,6 +910,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutItems.ShouldBeEmpty();

View File

@@ -12,9 +12,14 @@ namespace ErsatzTV.Core.Tests.Scheduling;
public class PlayoutModeSchedulerOneTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
_random = new Random();
}
private CancellationToken _cancellationToken;
private Random _random;
[Test]
public void Should_Have_Gap_With_No_Tail_No_Fallback()
@@ -51,6 +56,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(1));
@@ -134,6 +140,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(1));
@@ -202,6 +209,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -284,6 +292,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -356,6 +365,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
@@ -454,6 +464,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -558,6 +569,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -644,6 +656,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -744,6 +757,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
@@ -823,6 +837,7 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_random,
_cancellationToken);
playoutItems.ShouldBeEmpty();

View File

@@ -4,5 +4,5 @@ public class ProgramScheduleItemMultiple : ProgramScheduleItem
{
public MultipleMode MultipleMode { get; set; }
public int Count { get; set; }
public string Count { get; set; }
}

View File

@@ -11,5 +11,6 @@ public interface IPlayoutModeScheduler<in T> where T : ProgramScheduleItem
T scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,47 @@
using ErsatzTV.Core.Interfaces.Scheduling;
using NCalc;
namespace ErsatzTV.Core.Scheduling;
public static class CountExpression
{
public static int Evaluate(
string countExpression,
IMediaCollectionEnumerator enumerator,
Random random,
CancellationToken cancellationToken)
{
int enumeratorCount = enumerator is PlaylistEnumerator playlistEnumerator
? playlistEnumerator.CountForRandom
: enumerator.Count;
var expression = new Expression(countExpression);
expression.EvaluateParameter += (name, e) =>
{
e.Result = name switch
{
"count" => enumeratorCount,
"random" => enumeratorCount > 0 ? random.Next() % enumeratorCount : 0,
_ => e.Result
};
};
object expressionResult = 0;
try
{
expressionResult = expression.Evaluate(cancellationToken);
}
catch (Exception)
{
// do nothing
}
return expressionResult switch
{
double d when double.IsInfinity(d) || double.IsNaN(d) => 0,
double doubleResult => (int)Math.Floor(doubleResult),
int intResult => intResult,
long longResult => (int)longResult,
_ => 0
};
}
}

View File

@@ -857,6 +857,7 @@ public class PlayoutBuilder : IPlayoutBuilder
multiple,
nextScheduleItem,
playoutFinish,
random,
cancellationToken),
ProgramScheduleItemDuration duration => schedulerDuration.Schedule(
playoutBuilderState,
@@ -864,6 +865,7 @@ public class PlayoutBuilder : IPlayoutBuilder
duration,
nextScheduleItem,
playoutFinish,
random,
cancellationToken),
ProgramScheduleItemFlood flood => schedulerFlood.Schedule(
playoutBuilderState,
@@ -871,6 +873,7 @@ public class PlayoutBuilder : IPlayoutBuilder
flood,
nextScheduleItem,
playoutFinish,
random,
cancellationToken),
ProgramScheduleItemOne one => schedulerOne.Schedule(
playoutBuilderState,
@@ -878,6 +881,7 @@ public class PlayoutBuilder : IPlayoutBuilder
one,
nextScheduleItem,
playoutFinish,
random,
cancellationToken),
_ => throw new NotSupportedException(nameof(scheduleItem))
};

View File

@@ -23,6 +23,7 @@ public abstract class PlayoutModeSchedulerBase<T>(ILogger logger) : IPlayoutMode
T scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken);
public static DateTimeOffset GetFillerStartTimeAfter(

View File

@@ -16,6 +16,7 @@ public class PlayoutModeSchedulerDuration(ILogger logger)
ProgramScheduleItemDuration scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken)
{
var warnings = new PlayoutBuildWarnings();

View File

@@ -15,6 +15,7 @@ public class PlayoutModeSchedulerFlood(ILogger logger) : PlayoutModeSchedulerBas
ProgramScheduleItemFlood scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken)
{
var warnings = new PlayoutBuildWarnings();

View File

@@ -16,6 +16,7 @@ public class PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItem
ProgramScheduleItemMultiple scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken)
{
var warnings = new PlayoutBuildWarnings();
@@ -28,15 +29,16 @@ public class PlayoutModeSchedulerMultiple(Map<CollectionKey, int> collectionItem
return new PlayoutSchedulerResult(playoutBuilderState, playoutItems, warnings);
}
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
PlayoutBuilderState nextState = playoutBuilderState with
{
CurrentTime = firstStart,
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(
CountExpression.Evaluate(scheduleItem.Count, contentEnumerator, random, cancellationToken))
};
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
if (nextState.MultipleRemaining == 0)
{
switch (scheduleItem.MultipleMode)

View File

@@ -14,6 +14,7 @@ public class PlayoutModeSchedulerOne(ILogger logger) : PlayoutModeSchedulerBase<
ProgramScheduleItemOne scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop,
Random random,
CancellationToken cancellationToken)
{
var warnings = new PlayoutBuildWarnings();

View File

@@ -5,7 +5,6 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
using NCalc;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
@@ -34,27 +33,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
{
int seed = context.Playout.Seed + context.InstructionIndex + context.CurrentTime.DayOfYear;
var random = new Random(seed);
int enumeratorCount = enumerator is PlaylistEnumerator playlistEnumerator
? playlistEnumerator.CountForRandom
: enumerator.Count;
var expression = new Expression(count.Count);
expression.EvaluateParameter += (name, e) =>
{
e.Result = name switch
{
"count" => enumeratorCount,
"random" => enumeratorCount > 0 ? random.Next() % enumeratorCount : 0,
_ => e.Result
};
};
object expressionResult = expression.Evaluate(cancellationToken);
int countValue = expressionResult switch
{
double doubleResult => (int)Math.Floor(doubleResult),
int intResult => intResult,
_ => 0
};
int countValue = CountExpression.Evaluate(count.Count, enumerator, random, cancellationToken);
for (var i = 0; i < countValue; i++)
{

View File

@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Change_ProgramScheduleItemMultipleCountString : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Count",
table: "ProgramScheduleMultipleItem",
type: "longtext",
nullable: true,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Count",
table: "ProgramScheduleMultipleItem",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("ProductVersion", "9.0.12")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@@ -4130,8 +4130,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<int>("Count")
.HasColumnType("int");
b.Property<string>("Count")
.HasColumnType("longtext");
b.Property<int>("MultipleMode")
.HasColumnType("int");

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Change_ProgramScheduleItemMultipleCountString : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Count",
table: "ProgramScheduleMultipleItem",
type: "TEXT",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Count",
table: "ProgramScheduleMultipleItem",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@@ -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.11");
modelBuilder.HasAnnotation("ProductVersion", "9.0.12");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@@ -3957,8 +3957,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.Property<string>("Count")
.HasColumnType("TEXT");
b.Property<int>("MultipleMode")
.HasColumnType("INTEGER");

View File

@@ -532,7 +532,8 @@
</div>
<MudTextField @bind-Value="@_selectedItem.MultipleCount"
For="@(() => _selectedItem.MultipleCount)"
Disabled="@(_selectedItem.PlayoutMode is not PlayoutMode.Multiple || _selectedItem.MultipleMode is not MultipleMode.Count)"/>
Disabled="@(_selectedItem.PlayoutMode is not PlayoutMode.Multiple || _selectedItem.MultipleMode is not MultipleMode.Count)"
HelperText="Can be an expression with 'count' (collection size) and 'random' (0 to count-1)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">

View File

@@ -17,7 +17,7 @@ public class ProgramScheduleItemEditViewModelValidator : AbstractValidator<Progr
{
When(
i => i.MultipleMode is MultipleMode.Count,
() => RuleFor(i => i.MultipleCount).NotNull().GreaterThan(0));
() => RuleFor(i => i.MultipleCount).NotEmpty());
});
When(
i => i.PlayoutMode == PlayoutMode.Duration,

View File

@@ -16,7 +16,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
private CollectionType _collectionType;
private int? _discardToFillAttempts;
private FixedStartTimeBehavior? _fixedStartTimeBehavior;
private int? _multipleCount;
private string _multipleCount;
private PlaybackOrder _playbackOrder;
private TimeSpan? _playoutDuration;
private int _playoutDurationHours;
@@ -171,7 +171,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
public MultipleMode MultipleMode { get; set; }
public int? MultipleCount
public string MultipleCount
{
get => PlayoutMode == PlayoutMode.Multiple ? _multipleCount : null;
set => _multipleCount = value;