Browse Source

scheduling fixes (#451)

* scheduling fixes

* restore plex service

* restore plex service part 2
pull/453/head v0.2.0-alpha
Jason Dove 5 years ago committed by GitHub
parent
commit
a74547997d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  3. 175
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs
  4. 26
      ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs
  5. 30
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  6. 3657
      ErsatzTV.Infrastructure/Migrations/20211024113804_Fix_ProgramScheduleItemMidnightStartTime.Designer.cs
  7. 38
      ErsatzTV.Infrastructure/Migrations/20211024113804_Fix_ProgramScheduleItemMidnightStartTime.cs

2
CHANGELOG.md

@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix generated streams with mpeg2video
- Fix incorrect row count in playout detail table
- Fix deleting movies that have been removed from Jellyfin and Emby
- Fix bug that caused large unscheduled gaps in playouts
- This was caused by schedule items with a fixed start of midnight
### Added
- Add new filler system

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

@ -169,7 +169,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -169,7 +169,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -188,7 +188,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -188,7 +188,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -207,7 +207,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -207,7 +207,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -227,7 +227,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -227,7 +227,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -248,6 +248,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -248,6 +248,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
};
private static TimeSpan FixDuration(TimeSpan duration) =>
duration > TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
duration >= TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
}
}

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

@ -452,6 +452,181 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -452,6 +452,181 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
PostRollFiller = new FillerPreset
{
FillerKind = FillerKind.PostRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
}
[Test]
public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad_With_Chapters()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0), 2);
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
PostRollFiller = new FillerPreset
{
FillerKind = FillerKind.PostRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
enumerator3.State.Index.Should().Be(0);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
}
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{
StartTime = TimeSpan.FromHours(3)

26
ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs

@ -44,25 +44,39 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -44,25 +44,39 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 }
};
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
new()
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired, int chapterCount = 0)
{
var result = new Movie()
{
Id = id,
MovieMetadata = new List<MovieMetadata> { new() { ReleaseDate = aired } },
MediaVersions = new List<MediaVersion>
{
new() { Duration = duration }
new() { Duration = duration, Chapters = new List<MediaChapter>() }
}
};
protected static Collection TwoItemCollection(int id1, int id2, TimeSpan duration) => new()
for (var i = 0; i < chapterCount; i++)
{
result.MediaVersions.Head().Chapters.Add(
new MediaChapter
{
StartTime = TimeSpan.FromMilliseconds(i * duration.TotalMilliseconds / chapterCount),
EndTime = TimeSpan.FromMilliseconds(i + 1 * duration.TotalMilliseconds / chapterCount)
});
}
return result;
}
protected static Collection TwoItemCollection(int id1, int id2, TimeSpan duration, int chapterCount = 0) => new()
{
Id = id1,
Name = $"Collection of Items {id1}",
MediaItems = new List<MediaItem>
{
TestMovie(id1, duration, new DateTime(2020, 1, 1)),
TestMovie(id2, duration, new DateTime(2020, 1, 2))
TestMovie(id1, duration, new DateTime(2020, 1, 1), chapterCount),
TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount)
}
};

30
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -352,6 +352,12 @@ namespace ErsatzTV.Core.Scheduling @@ -352,6 +352,12 @@ namespace ErsatzTV.Core.Scheduling
return new List<PlayoutItem> { playoutItem };
}
List<MediaChapter> effectiveChapters = chapters;
if (allFiller.All(fp => fp.FillerKind != FillerKind.MidRoll))
{
effectiveChapters = new List<MediaChapter>();
}
foreach (FillerPreset filler in allFiller.Filter(
f => f.FillerKind == FillerKind.PreRoll && f.FillerMode != FillerMode.Pad))
{
@ -370,7 +376,7 @@ namespace ErsatzTV.Core.Scheduling @@ -370,7 +376,7 @@ namespace ErsatzTV.Core.Scheduling
}
}
if (allFiller.All(f => f.FillerKind != FillerKind.MidRoll) || !chapters.Any())
if (!effectiveChapters.Any())
{
result.Add(playoutItem);
}
@ -383,10 +389,10 @@ namespace ErsatzTV.Core.Scheduling @@ -383,10 +389,10 @@ namespace ErsatzTV.Core.Scheduling
{
case FillerMode.Duration when filler.Duration.HasValue:
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
for (var i = 0; i < chapters.Count; i++)
for (var i = 0; i < effectiveChapters.Count; i++)
{
result.Add(playoutItem.ForChapter(chapters[i]));
if (i < chapters.Count - 1)
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
if (i < effectiveChapters.Count - 1)
{
result.AddRange(
AddDurationFiller(
@ -400,9 +406,9 @@ namespace ErsatzTV.Core.Scheduling @@ -400,9 +406,9 @@ namespace ErsatzTV.Core.Scheduling
break;
case FillerMode.Count when filler.Count.HasValue:
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
for (var i = 0; i < chapters.Count - 1; i++)
for (var i = 0; i < effectiveChapters.Count - 1; i++)
{
result.Add(playoutItem.ForChapter(chapters[i]));
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
result.AddRange(
AddCountFiller(
playoutBuilderState,
@ -440,7 +446,7 @@ namespace ErsatzTV.Core.Scheduling @@ -440,7 +446,7 @@ namespace ErsatzTV.Core.Scheduling
var totalDuration =
TimeSpan.FromMilliseconds(
result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) +
chapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds));
effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds));
int currentMinute = (playoutItem.StartOffset + totalDuration).Minute;
// ReSharper disable once PossibleInvalidOperationException
@ -503,14 +509,14 @@ namespace ErsatzTV.Core.Scheduling @@ -503,14 +509,14 @@ namespace ErsatzTV.Core.Scheduling
mid1,
remainingToFill,
FillerKind.MidRoll));
TimeSpan average = chapters.Count == 0
TimeSpan average = effectiveChapters.Count == 0
? remainingToFill
: remainingToFill / (chapters.Count - 1);
: remainingToFill / (effectiveChapters.Count - 1);
TimeSpan filled = TimeSpan.Zero;
for (var i = 0; i < chapters.Count; i++)
{
result.Add(playoutItem.ForChapter(chapters[i]));
if (i < chapters.Count - 1)
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
if (i < effectiveChapters.Count - 1)
{
TimeSpan current = TimeSpan.Zero;
while (current < average && filled < remainingToFill)
@ -534,7 +540,7 @@ namespace ErsatzTV.Core.Scheduling @@ -534,7 +540,7 @@ namespace ErsatzTV.Core.Scheduling
playoutBuilderState,
enumerators,
scheduleItem,
i < chapters.Count - 1 ? maxThisBreak : leftOverall);
i < effectiveChapters.Count - 1 ? maxThisBreak : leftOverall);
foreach (PlayoutItem fallback in maybeFallback)
{

3657
ErsatzTV.Infrastructure/Migrations/20211024113804_Fix_ProgramScheduleItemMidnightStartTime.Designer.cs generated

File diff suppressed because it is too large Load Diff

38
ErsatzTV.Infrastructure/Migrations/20211024113804_Fix_ProgramScheduleItemMidnightStartTime.cs

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Fix_ProgramScheduleItemMidnightStartTime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"update ProgramScheduleItem set StartTime = '00:00:00' where StartTime = '1.00:00:00'");
migrationBuilder.Sql(
@"delete from PlayoutItem where PlayoutId in
(select Playout.Id from Playout
inner join ProgramSchedule PS on Playout.ProgramScheduleId = PS.Id
inner join ProgramScheduleItem PSI on PSI.ProgramScheduleId = PS.Id
where PSI.StartTime = '00:00:00')");
migrationBuilder.Sql(
@"delete from PlayoutProgramScheduleAnchor where PlayoutId in
(select Playout.Id from Playout
inner join ProgramSchedule PS on Playout.ProgramScheduleId = PS.Id
inner join ProgramScheduleItem PSI on PSI.ProgramScheduleId = PS.Id
where PSI.StartTime = '00:00:00')");
migrationBuilder.Sql(
@"UPDATE Playout SET Anchor_NextStart = null, Anchor_NextScheduleItemId = null where Id in
(select Playout.Id from Playout
inner join ProgramSchedule PS on Playout.ProgramScheduleId = PS.Id
inner join ProgramScheduleItem PSI on PSI.ProgramScheduleId = PS.Id
where PSI.StartTime = '00:00:00')");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
Loading…
Cancel
Save