scheduling fixes (#451)
* scheduling fixes * restore plex service * restore plex service part 2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiller.All(f => f.FillerKind != FillerKind.MidRoll) || !chapters.Any())
|
||||
if (!effectiveChapters.Any())
|
||||
{
|
||||
result.Add(playoutItem);
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
playoutBuilderState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
i < chapters.Count - 1 ? maxThisBreak : leftOverall);
|
||||
i < effectiveChapters.Count - 1 ? maxThisBreak : leftOverall);
|
||||
|
||||
foreach (PlayoutItem fallback in maybeFallback)
|
||||
{
|
||||
|
||||
+3657
File diff suppressed because it is too large
Load Diff
+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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user