diff --git a/CHANGELOG.md b/CHANGELOG.md index 022f2079..f9cdf8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day +- Fix playout bug that prevented padded durations from fitting within a schedule item of the same duration + - For example, filler that padded to 30 minutes would often not fit in a 30 minute duration schedule item - Fix VAAPI transcoding 8-bit source content to 10-bit - Fix NVIDIA subtitle scaling when `scale_npp` filter is unavailable - Remove ffmpeg and ffprobe as required dependencies for scanning media server libraries diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs index cd22f043..0290bcc9 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs @@ -1,10 +1,11 @@ -using ErsatzTV.Core.Domain; +using Destructurama; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Scheduling; using FluentAssertions; using Microsoft.Extensions.Logging; -using NSubstitute; using NUnit.Framework; +using Serilog; namespace ErsatzTV.Core.Tests.Scheduling; @@ -15,6 +16,20 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken; + private readonly ILogger _logger; + + public PlayoutModeSchedulerDurationTests() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .Destructure.UsingAttributes() + .CreateLogger(); + + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + + _logger = loggerFactory.CreateLogger(); + } [Test] public void Should_Fill_Exact_Duration() @@ -44,7 +59,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator), @@ -117,7 +132,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator), @@ -189,7 +204,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator), @@ -258,7 +273,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator), @@ -341,7 +356,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), @@ -428,7 +443,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), @@ -527,7 +542,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), @@ -637,7 +652,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators( @@ -710,6 +725,119 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); } + + [Test] + public void Should_Not_Have_Gap_With_Post_Roll_Pad_And_Fallback_Filler() + { + Collection collectionPre = TwoItemCollection(1, 2, TimeSpan.Parse("00:00:15.6734470")); + Collection collectionOne = TwoItemCollection(3, 4, TimeSpan.Parse("00:22:58.1220000")); + Collection collectionTwo = CollectionOf( + new Dictionary + { + { 5, TimeSpan.Parse("00:00:31.3004760") }, + { 6, TimeSpan.Parse("00:00:31.7880950") }, + { 7, TimeSpan.Parse("00:00:31.1147170") }, + { 8, TimeSpan.Parse("00:00:46.4863270") }, + { 9, TimeSpan.Parse("00:00:31.4165760") }, + { 10, TimeSpan.Parse("00:00:31.5791160") }, + { 11, TimeSpan.Parse("00:00:31.2540360") }, + { 12, TimeSpan.Parse("00:00:36.2231070") }, + { 13, TimeSpan.Parse("00:02:00.0471430") }, + }); + Collection collectionThree = TwoItemCollection(14, 15, TimeSpan.Parse("00:00:55.6349890")); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromMinutes(30), + PlaybackOrder = PlaybackOrder.Chronological, + PreRollFiller = new FillerPreset + { + FillerKind = FillerKind.PreRoll, + FillerMode = FillerMode.Count, + Count = 1, + Collection = collectionPre, + CollectionId = collectionPre.Id + }, + 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 scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionPre.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator4 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(_logger); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator2, + scheduleItem.PreRollFiller, + enumerator1, + scheduleItem.PostRollFiller, + enumerator3, + scheduleItem.FallbackFiller, + enumerator4), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator), + _cancellationToken); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddMinutes(30)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + // THIS IS THE KEY TEST - needs to be exactly 30 minutes + (playoutItems.Last().FinishOffset - playoutItems.First().StartOffset).Should().Be(TimeSpan.FromMinutes(30)); + + // playoutBuilderState.NextGuideGroup.Should().Be(3); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + enumerator3.State.Index.Should().Be(0); + enumerator4.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(12); + } [Test] public void Should_Not_Schedule_At_HardStop() @@ -745,7 +873,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(Substitute.For()); + var scheduler = new PlayoutModeSchedulerDuration(_logger); (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( startState, CollectionEnumerators(scheduleItem, enumerator), diff --git a/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs b/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs index b1fc2501..c8bd3378 100644 --- a/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs +++ b/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs @@ -45,6 +45,23 @@ public abstract class SchedulerTestBase { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }, { CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 } }; + + protected static Dictionary CollectionEnumerators( + ProgramScheduleItem scheduleItem, + IMediaCollectionEnumerator enumerator1, + FillerPreset fillerPreset, + IMediaCollectionEnumerator enumerator2, + FillerPreset fillerPreset2, + IMediaCollectionEnumerator enumerator3, + FillerPreset fillerPreset3, + IMediaCollectionEnumerator enumerator4) => + new() + { + { CollectionKey.ForScheduleItem(scheduleItem), enumerator1 }, + { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }, + { CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 }, + { CollectionKey.ForFillerPreset(fillerPreset3), enumerator4 } + }; private static Movie TestMovie(int id, TimeSpan duration, DateTime aired, int chapterCount = 0) { @@ -81,6 +98,22 @@ public abstract class SchedulerTestBase TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount) } }; + + protected static Collection CollectionOf(IDictionary idsAndDurations, int chapterCount = 0) + { + var mediaItems = new List(); + foreach ((int id, TimeSpan duration) in idsAndDurations) + { + mediaItems.Add(TestMovie(id, duration, new DateTime(2020, 1, id), chapterCount)); + } + + return new Collection + { + Id = idsAndDurations.Head().Key, + Name = $"Collection of Items {idsAndDurations.Head().Key}", + MediaItems = mediaItems + }; + } protected static Dictionary CollectionEnumerators( ProgramScheduleItem scheduleItem, diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs index da47800c..706111cd 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs @@ -41,7 +41,6 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe public static DateTimeOffset GetStartTimeAfter(PlayoutBuilderState state, ProgramScheduleItem scheduleItem) { DateTimeOffset startTime = state.CurrentTime.ToLocalTime(); - startTime = startTime.AddTicks(-(startTime.Ticks % TimeSpan.TicksPerSecond)); bool isIncomplete = scheduleItem is ProgramScheduleItemMultiple && state.MultipleRemaining.IsSome || scheduleItem is ProgramScheduleItemDuration && state.DurationFinish.IsSome || @@ -51,7 +50,6 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe if (scheduleItem.StartType == StartType.Fixed && !isIncomplete) { TimeSpan itemStartTime = scheduleItem.StartTime.GetValueOrDefault(); - itemStartTime = TimeSpan.FromMilliseconds((int)itemStartTime.TotalMilliseconds); DateTime date = startTime.Date; DateTimeOffset result = new DateTimeOffset( @@ -352,13 +350,12 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe foreach (FillerPreset padFiller in Optional( allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue))) { - var totalDuration = TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds)); + var totalDuration = TimeSpan.FromTicks(result.Sum(pi => (pi.Finish - pi.Start).Ticks)); // add primary content to totalDuration only if it hasn't already been added if (result.All(pi => pi.MediaItemId != playoutItem.MediaItemId)) { - totalDuration += TimeSpan.FromMilliseconds( - effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds)); + totalDuration += TimeSpan.FromTicks(effectiveChapters.Sum(c => (c.EndTime - c.StartTime).Ticks)); } int currentMinute = (playoutItem.StartOffset + totalDuration).Minute; @@ -402,8 +399,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe padFiller.AllowWatermarks, log, cancellationToken)); - totalDuration = - TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds)); + totalDuration = TimeSpan.FromTicks(result.Sum(pi => (pi.Finish - pi.Start).Ticks)); remainingToFill = targetTime - totalDuration - playoutItem.StartOffset; if (remainingToFill > TimeSpan.Zero) { @@ -493,8 +489,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe padFiller.AllowWatermarks, log, cancellationToken)); - totalDuration = - TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds)); + totalDuration = TimeSpan.FromTicks(result.Sum(pi => (pi.Finish - pi.Start).Ticks)); remainingToFill = targetTime - totalDuration - playoutItem.StartOffset; if (remainingToFill > TimeSpan.Zero) { diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index 3d2259df..09f72562 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -167,7 +167,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase scheduleItem.PlayoutDuration) + if (durationBlock > scheduleItem.PlayoutDuration) { Logger.LogWarning( "Unable to schedule duration block of {DurationBlock:hh\\:mm\\:ss} which is longer than the configured playout duration {PlayoutDuration:hh\\:mm\\:ss}", diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index e9dce2e8..f0ad7220 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -86,9 +86,11 @@ public class WorkerService : BackgroundService case MatchTraktListItems matchTraktListItems: await mediator.Send(matchTraktListItems, stoppingToken); break; +#if !DEBUG_NO_SYNC case ExtractEmbeddedSubtitles extractEmbeddedSubtitles: await mediator.Send(extractEmbeddedSubtitles, stoppingToken); break; +#endif case ReleaseMemory aggressivelyReleaseMemory: await mediator.Send(aggressivelyReleaseMemory, stoppingToken); break; diff --git a/scripts/set-provider.sh b/scripts/set-provider.sh new file mode 100755 index 00000000..1a38b43a --- /dev/null +++ b/scripts/set-provider.sh @@ -0,0 +1,12 @@ +#! /usr/bin/env bash + +if [[ $# -eq 0 ]] ; then + echo 'Please specify a database provider' + exit 1 +fi + +cd "$(git rev-parse --show-cdup)" || exit +cd ErsatzTV && dotnet user-secrets set "Provider" "$1" + +cd "$(git rev-parse --show-cdup)" || exit +cd ErsatzTV.Scanner && dotnet user-secrets set "Provider" "$1"