Browse Source

make count an expression in classic schedules (#2794)

* make count an expression in classic schedules

* add tests
pull/2795/head
Jason Dove 6 months ago committed by GitHub
parent
commit
08ceb53b2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 22
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  3. 2
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  4. 6
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  5. 2
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  6. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  7. 4
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs
  8. 12
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs
  9. 34
      ErsatzTV.Core.Tests/Scheduling/CountExpressionTests.cs
  10. 1
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  11. 17
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs
  12. 20
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs
  13. 37
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  14. 17
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs
  15. 2
      ErsatzTV.Core/Domain/ProgramScheduleItemMultiple.cs
  16. 1
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutModeScheduler.cs
  17. 47
      ErsatzTV.Core/Scheduling/CountExpression.cs
  18. 4
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  19. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  20. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  21. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  22. 10
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  23. 1
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  24. 23
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  25. 7024
      ErsatzTV.Infrastructure.MySql/Migrations/20260120145601_Change_ProgramScheduleItemMultipleCountString.Designer.cs
  26. 38
      ErsatzTV.Infrastructure.MySql/Migrations/20260120145601_Change_ProgramScheduleItemMultipleCountString.cs
  27. 6
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  28. 6851
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260120145518_Change_ProgramScheduleItemMultipleCountString.Designer.cs
  29. 36
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260120145518_Change_ProgramScheduleItemMultipleCountString.cs
  30. 6
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  31. 3
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  32. 2
      ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs
  33. 4
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

22
CHANGELOG.md

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

2
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

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

2
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

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

6
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -79,10 +79,10 @@ public abstract class ProgramScheduleItemCommandBase
"[MultipleMode] cannot be [PlaylistItemSize] when collection is not a playlist"); "[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( return BaseError.New(
"[MultipleCount] must be greater than 0 for playout mode 'multiple / count'"); "[MultipleCount] must be valid for playout mode 'multiple / count'");
} }
break; break;
@ -298,7 +298,7 @@ public abstract class ProgramScheduleItemCommandBase
MarathonBatchSize = item.MarathonBatchSize, MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = item.FillWithGroupMode, FillWithGroupMode = item.FillWithGroupMode,
MultipleMode = item.MultipleMode, 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, CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode, GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId, PreRollFillerId = item.PreRollFillerId,

2
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

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

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

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

4
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs

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

12
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs

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

34
ErsatzTV.Core.Tests/Scheduling/CountExpressionTests.cs

@ -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);
}
}

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

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

17
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs

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

20
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs

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

37
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs

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

17
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs

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

2
ErsatzTV.Core/Domain/ProgramScheduleItemMultiple.cs

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

1
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutModeScheduler.cs

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

47
ErsatzTV.Core/Scheduling/CountExpression.cs

@ -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
};
}
}

4
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

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

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

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

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

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

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

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

10
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

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

1
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

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

23
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs

@ -5,7 +5,6 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NCalc;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; 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; int seed = context.Playout.Seed + context.InstructionIndex + context.CurrentTime.DayOfYear;
var random = new Random(seed); var random = new Random(seed);
int enumeratorCount = enumerator is PlaylistEnumerator playlistEnumerator int countValue = CountExpression.Evaluate(count.Count, enumerator, random, cancellationToken);
? 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
};
for (var i = 0; i < countValue; i++) for (var i = 0; i < countValue; i++)
{ {

7024
ErsatzTV.Infrastructure.MySql/Migrations/20260120145601_Change_ProgramScheduleItemMultipleCountString.Designer.cs generated

File diff suppressed because it is too large Load Diff

38
ErsatzTV.Infrastructure.MySql/Migrations/20260120145601_Change_ProgramScheduleItemMultipleCountString.cs

@ -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");
}
}
}

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

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

6851
ErsatzTV.Infrastructure.Sqlite/Migrations/20260120145518_Change_ProgramScheduleItemMultipleCountString.Designer.cs generated

File diff suppressed because it is too large Load Diff

36
ErsatzTV.Infrastructure.Sqlite/Migrations/20260120145518_Change_ProgramScheduleItemMultipleCountString.cs

@ -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);
}
}
}

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

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); modelBuilder.HasAnnotation("ProductVersion", "9.0.12");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{ {
@ -3957,8 +3957,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{ {
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<int>("Count") b.Property<string>("Count")
.HasColumnType("INTEGER"); .HasColumnType("TEXT");
b.Property<int>("MultipleMode") b.Property<int>("MultipleMode")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

3
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -532,7 +532,8 @@
</div> </div>
<MudTextField @bind-Value="@_selectedItem.MultipleCount" <MudTextField @bind-Value="@_selectedItem.MultipleCount"
For="@(() => _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>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">

2
ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs

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

4
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

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

Loading…
Cancel
Save