rework pad and duration filler (#1304)
This commit is contained in:
@@ -16,6 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
### Fixed
|
### Fixed
|
||||||
- Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item
|
- Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- For `Pad` and `Duration` filler - prioritize filling the configured pad/duration
|
||||||
|
- This will skip filler that is too long in an attempt to avoid unscheduled time
|
||||||
|
- You may see the same filler more often, which means you may want to add more filler to your library so ETV has more options
|
||||||
|
|
||||||
## [0.7.9-beta] - 2023-06-10
|
## [0.7.9-beta] - 2023-06-10
|
||||||
### Added
|
### Added
|
||||||
- Synchronize actor metadata from Jellyfin and Emby television libraries
|
- Synchronize actor metadata from Jellyfin and Emby television libraries
|
||||||
|
|||||||
@@ -23,189 +23,6 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
|||||||
private CancellationToken _cancellationToken;
|
private CancellationToken _cancellationToken;
|
||||||
private PlayoutModeSchedulerBase<ProgramScheduleItem> _scheduler;
|
private PlayoutModeSchedulerBase<ProgramScheduleItem> _scheduler;
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class CalculateEndTimeWithFiller : PlayoutModeSchedulerBaseTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public void Should_Not_Touch_Enumerator()
|
|
||||||
{
|
|
||||||
var collection = new Collection
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
Name = "Filler Items",
|
|
||||||
MediaItems = new List<MediaItem>()
|
|
||||||
};
|
|
||||||
|
|
||||||
for (var i = 0; i < 5; i++)
|
|
||||||
{
|
|
||||||
collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var fillerPreset = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.PreRoll,
|
|
||||||
FillerMode = FillerMode.Count,
|
|
||||||
Count = 3,
|
|
||||||
Collection = collection,
|
|
||||||
CollectionId = collection.Id
|
|
||||||
};
|
|
||||||
|
|
||||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
|
||||||
collection.MediaItems,
|
|
||||||
new CollectionEnumeratorState { Index = 0, Seed = 1 });
|
|
||||||
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>
|
|
||||||
{
|
|
||||||
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator }
|
|
||||||
},
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
PreRollFiller = fillerPreset
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5)));
|
|
||||||
enumerator.State.Index.Should().Be(0);
|
|
||||||
enumerator.State.Seed.Should().Be(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Should_Pad_To_15_Minutes_15()
|
|
||||||
{
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
MidRollFiller = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.MidRoll,
|
|
||||||
FillerMode = FillerMode.Pad,
|
|
||||||
PadToNearestMinute = 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Should_Pad_To_15_Minutes_30()
|
|
||||||
{
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
MidRollFiller = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.MidRoll,
|
|
||||||
FillerMode = FillerMode.Pad,
|
|
||||||
PadToNearestMinute = 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Should_Pad_To_15_Minutes_45()
|
|
||||||
{
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
MidRollFiller = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.MidRoll,
|
|
||||||
FillerMode = FillerMode.Pad,
|
|
||||||
PadToNearestMinute = 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Should_Pad_To_15_Minutes_00()
|
|
||||||
{
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
MidRollFiller = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.MidRoll,
|
|
||||||
FillerMode = FillerMode.Pad,
|
|
||||||
PadToNearestMinute = 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Should_Pad_To_30_Minutes_30()
|
|
||||||
{
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
MidRollFiller = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.MidRoll,
|
|
||||||
FillerMode = FillerMode.Pad,
|
|
||||||
PadToNearestMinute = 30
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Should_Pad_To_30_Minutes_00()
|
|
||||||
{
|
|
||||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
|
||||||
.CalculateEndTimeWithFiller(
|
|
||||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
|
||||||
new ProgramScheduleItemOne
|
|
||||||
{
|
|
||||||
MidRollFiller = new FillerPreset
|
|
||||||
{
|
|
||||||
FillerKind = FillerKind.MidRoll,
|
|
||||||
FillerMode = FillerMode.Pad,
|
|
||||||
PadToNearestMinute = 30
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)),
|
|
||||||
new TimeSpan(0, 12, 30),
|
|
||||||
new List<MediaChapter>());
|
|
||||||
|
|
||||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class AddFiller : PlayoutModeSchedulerBaseTests
|
public class AddFiller : PlayoutModeSchedulerBaseTests
|
||||||
{
|
{
|
||||||
@@ -241,6 +58,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
|||||||
scheduleItem,
|
scheduleItem,
|
||||||
new PlayoutItem(),
|
new PlayoutItem(),
|
||||||
new List<MediaChapter>(),
|
new List<MediaChapter>(),
|
||||||
|
log: true,
|
||||||
_cancellationToken);
|
_cancellationToken);
|
||||||
|
|
||||||
playoutItems.Count.Should().Be(1);
|
playoutItems.Count.Should().Be(1);
|
||||||
@@ -293,6 +111,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
|||||||
scheduleItem,
|
scheduleItem,
|
||||||
new PlayoutItem(),
|
new PlayoutItem(),
|
||||||
new List<MediaChapter> { new() },
|
new List<MediaChapter> { new() },
|
||||||
|
log: true,
|
||||||
_cancellationToken);
|
_cancellationToken);
|
||||||
|
|
||||||
playoutItems.Count.Should().Be(1);
|
playoutItems.Count.Should().Be(1);
|
||||||
@@ -359,6 +178,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
|||||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) }
|
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) }
|
||||||
},
|
},
|
||||||
|
log: true,
|
||||||
_cancellationToken);
|
_cancellationToken);
|
||||||
|
|
||||||
playoutItems.Count.Should().Be(3);
|
playoutItems.Count.Should().Be(3);
|
||||||
@@ -450,6 +270,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
|||||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
|
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
|
||||||
},
|
},
|
||||||
|
log: true,
|
||||||
_cancellationToken);
|
_cancellationToken);
|
||||||
|
|
||||||
playoutItems.Count.Should().Be(5);
|
playoutItems.Count.Should().Be(5);
|
||||||
@@ -555,6 +376,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
|||||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
|
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
|
||||||
},
|
},
|
||||||
|
log: true,
|
||||||
_cancellationToken);
|
_cancellationToken);
|
||||||
|
|
||||||
playoutItems.Count.Should().Be(5);
|
playoutItems.Count.Should().Be(5);
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
|
||||||
using ErsatzTV.Core.Scheduling;
|
|
||||||
using FluentAssertions;
|
|
||||||
using LanguageExt.UnsafeValueAccess;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Tests.Scheduling;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class ShuffledMediaCollectionEnumeratorTests
|
|
||||||
{
|
|
||||||
[SetUp]
|
|
||||||
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
|
|
||||||
|
|
||||||
private readonly List<GroupedMediaItem> _mediaItems = new()
|
|
||||||
{
|
|
||||||
new GroupedMediaItem(new MediaItem { Id = 1 }, new List<MediaItem>()),
|
|
||||||
new GroupedMediaItem(new MediaItem { Id = 2 }, new List<MediaItem>()),
|
|
||||||
new GroupedMediaItem(new MediaItem { Id = 3 }, new List<MediaItem>())
|
|
||||||
};
|
|
||||||
|
|
||||||
private CancellationToken _cancellationToken;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Peek_Zero_Should_Match_Current()
|
|
||||||
{
|
|
||||||
var state = new CollectionEnumeratorState { Index = 0, Seed = 0 };
|
|
||||||
var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken);
|
|
||||||
|
|
||||||
Option<MediaItem> peek = enumerator.Peek(0);
|
|
||||||
Option<MediaItem> current = enumerator.Current;
|
|
||||||
|
|
||||||
peek.IsSome.Should().BeTrue();
|
|
||||||
current.IsSome.Should().BeTrue();
|
|
||||||
peek.ValueUnsafe().Id.Should().Be(1);
|
|
||||||
current.ValueUnsafe().Id.Should().Be(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Peek_One_Should_Match_Next()
|
|
||||||
{
|
|
||||||
var state = new CollectionEnumeratorState { Index = 0, Seed = 0 };
|
|
||||||
var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken);
|
|
||||||
|
|
||||||
Option<MediaItem> peek = enumerator.Peek(1);
|
|
||||||
|
|
||||||
enumerator.MoveNext();
|
|
||||||
Option<MediaItem> next = enumerator.Current;
|
|
||||||
|
|
||||||
peek.IsSome.Should().BeTrue();
|
|
||||||
next.IsSome.Should().BeTrue();
|
|
||||||
peek.ValueUnsafe().Id.Should().Be(2);
|
|
||||||
next.ValueUnsafe().Id.Should().Be(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Peek_Two_Should_Match_NextNext()
|
|
||||||
{
|
|
||||||
var state = new CollectionEnumeratorState { Index = 0, Seed = 0 };
|
|
||||||
var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken);
|
|
||||||
|
|
||||||
Option<MediaItem> peek = enumerator.Peek(2);
|
|
||||||
|
|
||||||
enumerator.MoveNext();
|
|
||||||
enumerator.MoveNext();
|
|
||||||
Option<MediaItem> next = enumerator.Current;
|
|
||||||
|
|
||||||
peek.IsSome.Should().BeTrue();
|
|
||||||
next.IsSome.Should().BeTrue();
|
|
||||||
peek.ValueUnsafe().Id.Should().Be(3);
|
|
||||||
next.ValueUnsafe().Id.Should().Be(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Peek_Three_Should_Match_NextNextNext()
|
|
||||||
{
|
|
||||||
var state = new CollectionEnumeratorState { Index = 0, Seed = 0 };
|
|
||||||
var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken);
|
|
||||||
|
|
||||||
Option<MediaItem> peek = enumerator.Peek(3);
|
|
||||||
|
|
||||||
enumerator.MoveNext();
|
|
||||||
enumerator.MoveNext();
|
|
||||||
enumerator.MoveNext();
|
|
||||||
Option<MediaItem> next = enumerator.Current;
|
|
||||||
|
|
||||||
peek.IsSome.Should().BeTrue();
|
|
||||||
next.IsSome.Should().BeTrue();
|
|
||||||
peek.ValueUnsafe().Id.Should().Be(2);
|
|
||||||
next.ValueUnsafe().Id.Should().Be(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,4 +4,5 @@ public class CollectionEnumeratorState
|
|||||||
{
|
{
|
||||||
public int Seed { get; set; }
|
public int Seed { get; set; }
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
public CollectionEnumeratorState Clone() => new CollectionEnumeratorState { Seed = Seed, Index = Index };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
|
|||||||
|
|
||||||
public interface IMediaCollectionEnumerator
|
public interface IMediaCollectionEnumerator
|
||||||
{
|
{
|
||||||
|
IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken);
|
||||||
CollectionEnumeratorState State { get; }
|
CollectionEnumeratorState State { get; }
|
||||||
Option<MediaItem> Current { get; }
|
Option<MediaItem> Current { get; }
|
||||||
void MoveNext();
|
void MoveNext();
|
||||||
Option<MediaItem> Peek(int offset);
|
int Count { get; }
|
||||||
Option<TimeSpan> MinimumDuration { get; }
|
Option<TimeSpan> MinimumDuration { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,18 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new ChronologicalMediaCollectionEnumerator(_sortedMediaItems, state);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||||
|
|
||||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset) =>
|
|
||||||
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
|
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _sortedMediaItems.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ namespace ErsatzTV.Core.Scheduling;
|
|||||||
|
|
||||||
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||||
{
|
{
|
||||||
|
private readonly Collection _collection;
|
||||||
|
private readonly IList<MediaItem> _mediaItems;
|
||||||
|
|
||||||
private readonly IList<MediaItem> _sortedMediaItems;
|
private readonly IList<MediaItem> _sortedMediaItems;
|
||||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||||
|
|
||||||
@@ -14,6 +17,9 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
IList<MediaItem> mediaItems,
|
IList<MediaItem> mediaItems,
|
||||||
CollectionEnumeratorState state)
|
CollectionEnumeratorState state)
|
||||||
{
|
{
|
||||||
|
_collection = collection;
|
||||||
|
_mediaItems = mediaItems;
|
||||||
|
|
||||||
// TODO: this will break if we allow shows and seasons
|
// TODO: this will break if we allow shows and seasons
|
||||||
_sortedMediaItems = collection.CollectionItems
|
_sortedMediaItems = collection.CollectionItems
|
||||||
.OrderBy(ci => ci.CustomIndex)
|
.OrderBy(ci => ci.CustomIndex)
|
||||||
@@ -29,14 +35,18 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new CustomOrderCollectionEnumerator(_collection, _mediaItems, state);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||||
|
|
||||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset) =>
|
|
||||||
throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _sortedMediaItems.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||||
using LanguageExt.UnsafeValueAccess;
|
using LanguageExt.UnsafeValueAccess;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Scheduling;
|
namespace ErsatzTV.Core.Scheduling;
|
||||||
|
|
||||||
@@ -195,142 +197,13 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
PlayoutBuilder.DisplayTitle(mediaItem),
|
PlayoutBuilder.DisplayTitle(mediaItem),
|
||||||
startTime);
|
startTime);
|
||||||
|
|
||||||
internal static DateTimeOffset CalculateEndTimeWithFiller(
|
|
||||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
|
||||||
ProgramScheduleItem scheduleItem,
|
|
||||||
DateTimeOffset itemStartTime,
|
|
||||||
TimeSpan itemDuration,
|
|
||||||
List<MediaChapter> chapters)
|
|
||||||
{
|
|
||||||
var allFiller = Optional(scheduleItem.PreRollFiller)
|
|
||||||
.Append(Optional(scheduleItem.MidRollFiller))
|
|
||||||
.Append(Optional(scheduleItem.PostRollFiller))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
|
||||||
if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1)
|
|
||||||
{
|
|
||||||
return itemStartTime + itemDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSpan totalDuration = itemDuration;
|
|
||||||
foreach (FillerPreset filler in allFiller)
|
|
||||||
{
|
|
||||||
switch (filler.FillerKind, filler.FillerMode)
|
|
||||||
{
|
|
||||||
case (FillerKind.MidRoll, FillerMode.Duration) when filler.Duration.HasValue:
|
|
||||||
IMediaCollectionEnumerator mrde = enumerators[CollectionKey.ForFillerPreset(filler)];
|
|
||||||
var mrdePeekOffset = 0;
|
|
||||||
for (var i = 0; i < chapters.Count - 1; i++)
|
|
||||||
{
|
|
||||||
TimeSpan midRollDuration = filler.Duration.Value;
|
|
||||||
while (mrde.Peek(mrdePeekOffset))
|
|
||||||
{
|
|
||||||
foreach (MediaItem mediaItem in mrde.Peek(mrdePeekOffset))
|
|
||||||
{
|
|
||||||
TimeSpan currentDuration = DurationForMediaItem(mediaItem);
|
|
||||||
midRollDuration -= currentDuration;
|
|
||||||
if (midRollDuration >= TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
totalDuration += currentDuration;
|
|
||||||
mrdePeekOffset++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (midRollDuration < TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case (FillerKind.MidRoll, FillerMode.Count) when filler.Count.HasValue:
|
|
||||||
IMediaCollectionEnumerator mrce = enumerators[CollectionKey.ForFillerPreset(filler)];
|
|
||||||
var mrcePeekOffset = 0;
|
|
||||||
for (var i = 0; i < chapters.Count - 1; i++)
|
|
||||||
{
|
|
||||||
for (var j = 0; j < filler.Count.Value; j++)
|
|
||||||
{
|
|
||||||
foreach (MediaItem mediaItem in mrce.Peek(mrcePeekOffset))
|
|
||||||
{
|
|
||||||
totalDuration += DurationForMediaItem(mediaItem);
|
|
||||||
mrcePeekOffset++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case (_, FillerMode.Duration) when filler.Duration.HasValue:
|
|
||||||
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
|
||||||
var peekOffset1 = 0;
|
|
||||||
TimeSpan duration = filler.Duration.Value;
|
|
||||||
while (e1.Peek(peekOffset1).IsSome)
|
|
||||||
{
|
|
||||||
foreach (MediaItem mediaItem in e1.Peek(peekOffset1))
|
|
||||||
{
|
|
||||||
TimeSpan currentDuration = DurationForMediaItem(mediaItem);
|
|
||||||
duration -= currentDuration;
|
|
||||||
if (duration >= TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
totalDuration += currentDuration;
|
|
||||||
peekOffset1++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration < TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case (_, FillerMode.Count) when filler.Count.HasValue:
|
|
||||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
|
||||||
var peekOffset2 = 0;
|
|
||||||
for (var i = 0; i < filler.Count.Value; i++)
|
|
||||||
{
|
|
||||||
foreach (MediaItem mediaItem in e2.Peek(peekOffset2))
|
|
||||||
{
|
|
||||||
totalDuration += DurationForMediaItem(mediaItem);
|
|
||||||
peekOffset2++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (FillerPreset padFiller in Optional(
|
|
||||||
allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue)))
|
|
||||||
{
|
|
||||||
int currentMinute = (itemStartTime + totalDuration).Minute;
|
|
||||||
// ReSharper disable once PossibleInvalidOperationException
|
|
||||||
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
|
|
||||||
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
|
|
||||||
|
|
||||||
DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) +
|
|
||||||
TimeSpan.FromMinutes(targetMinute);
|
|
||||||
|
|
||||||
return new DateTimeOffset(
|
|
||||||
targetTime.Year,
|
|
||||||
targetTime.Month,
|
|
||||||
targetTime.Day,
|
|
||||||
targetTime.Hour,
|
|
||||||
targetTime.Minute,
|
|
||||||
0,
|
|
||||||
targetTime.Offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemStartTime + totalDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal List<PlayoutItem> AddFiller(
|
internal List<PlayoutItem> AddFiller(
|
||||||
PlayoutBuilderState playoutBuilderState,
|
PlayoutBuilderState playoutBuilderState,
|
||||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||||
ProgramScheduleItem scheduleItem,
|
ProgramScheduleItem scheduleItem,
|
||||||
PlayoutItem playoutItem,
|
PlayoutItem playoutItem,
|
||||||
List<MediaChapter> chapters,
|
List<MediaChapter> chapters,
|
||||||
|
bool log,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = new List<PlayoutItem>();
|
var result = new List<PlayoutItem>();
|
||||||
@@ -367,6 +240,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
filler.Duration.Value,
|
filler.Duration.Value,
|
||||||
FillerKind.PreRoll,
|
FillerKind.PreRoll,
|
||||||
filler.AllowWatermarks,
|
filler.AllowWatermarks,
|
||||||
|
log,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
break;
|
break;
|
||||||
case FillerMode.Count when filler.Count.HasValue:
|
case FillerMode.Count when filler.Count.HasValue:
|
||||||
@@ -408,6 +282,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
filler.Duration.Value,
|
filler.Duration.Value,
|
||||||
FillerKind.MidRoll,
|
FillerKind.MidRoll,
|
||||||
filler.AllowWatermarks,
|
filler.AllowWatermarks,
|
||||||
|
log,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,6 +325,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
filler.Duration.Value,
|
filler.Duration.Value,
|
||||||
FillerKind.PostRoll,
|
FillerKind.PostRoll,
|
||||||
filler.AllowWatermarks,
|
filler.AllowWatermarks,
|
||||||
|
log,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
break;
|
break;
|
||||||
case FillerMode.Count when filler.Count.HasValue:
|
case FillerMode.Count when filler.Count.HasValue:
|
||||||
@@ -518,6 +394,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
remainingToFill,
|
remainingToFill,
|
||||||
FillerKind.PreRoll,
|
FillerKind.PreRoll,
|
||||||
padFiller.AllowWatermarks,
|
padFiller.AllowWatermarks,
|
||||||
|
log,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
totalDuration =
|
totalDuration =
|
||||||
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
||||||
@@ -544,6 +421,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
remainingToFill,
|
remainingToFill,
|
||||||
FillerKind.MidRoll,
|
FillerKind.MidRoll,
|
||||||
padFiller.AllowWatermarks,
|
padFiller.AllowWatermarks,
|
||||||
|
log,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
TimeSpan average = effectiveChapters.Count <= 1
|
TimeSpan average = effectiveChapters.Count <= 1
|
||||||
? remainingToFill
|
? remainingToFill
|
||||||
@@ -607,6 +485,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
remainingToFill,
|
remainingToFill,
|
||||||
FillerKind.PostRoll,
|
FillerKind.PostRoll,
|
||||||
padFiller.AllowWatermarks,
|
padFiller.AllowWatermarks,
|
||||||
|
log,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
totalDuration =
|
totalDuration =
|
||||||
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
||||||
@@ -682,13 +561,15 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
TimeSpan duration,
|
TimeSpan duration,
|
||||||
FillerKind fillerKind,
|
FillerKind fillerKind,
|
||||||
bool allowWatermarks,
|
bool allowWatermarks,
|
||||||
|
bool log,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = new List<PlayoutItem>();
|
var result = new List<PlayoutItem>();
|
||||||
|
|
||||||
TimeSpan remainingToFill = duration;
|
TimeSpan remainingToFill = duration;
|
||||||
var skipped = false;
|
var discardToFillAttempts = 0;
|
||||||
while (enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero)
|
while (enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero &&
|
||||||
|
remainingToFill >= enumerator.MinimumDuration)
|
||||||
{
|
{
|
||||||
foreach (MediaItem mediaItem in enumerator.Current)
|
foreach (MediaItem mediaItem in enumerator.Current)
|
||||||
{
|
{
|
||||||
@@ -712,40 +593,30 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
|||||||
result.Add(playoutItem);
|
result.Add(playoutItem);
|
||||||
enumerator.MoveNext();
|
enumerator.MoveNext();
|
||||||
}
|
}
|
||||||
else if (skipped)
|
|
||||||
{
|
|
||||||
// set to zero so it breaks out of the while loop
|
|
||||||
remainingToFill = TimeSpan.Zero;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (itemDuration >= duration * 1.5)
|
discardToFillAttempts++;
|
||||||
|
if (discardToFillAttempts >= enumerator.Count)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
// set to zero so it breaks out of the while loop
|
||||||
"Filler item is too long {FillerDuration} to fill {GapDuration}; skipping to next filler item",
|
remainingToFill = TimeSpan.Zero;
|
||||||
itemDuration,
|
|
||||||
duration);
|
|
||||||
|
|
||||||
skipped = true;
|
|
||||||
enumerator.MoveNext();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (itemDuration > duration)
|
if (log)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogDebug(
|
||||||
"Filler item is too long {FillerDuration} to fill {GapDuration}; aborting filler block",
|
"Filler item is too long {FillerDuration:g} to fill {GapDuration:g}; skipping to next filler item",
|
||||||
itemDuration,
|
itemDuration,
|
||||||
duration);
|
remainingToFill);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set to zero so it breaks out of the while loop
|
enumerator.MoveNext();
|
||||||
remainingToFill = TimeSpan.Zero;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,25 +121,41 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||||
|
|
||||||
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
||||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
|
||||||
collectionEnumerators,
|
var enumeratorClones = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||||
|
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||||
|
{
|
||||||
|
IMediaCollectionEnumerator clone = enumerator.Clone(enumerator.State.Clone(), cancellationToken);
|
||||||
|
enumeratorClones.Add(key, clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlayoutItem> maybePlayoutItems = AddFiller(
|
||||||
|
nextState,
|
||||||
|
enumeratorClones,
|
||||||
scheduleItem,
|
scheduleItem,
|
||||||
itemStartTime,
|
playoutItem,
|
||||||
itemDuration,
|
itemChapters,
|
||||||
itemChapters);
|
log: false,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
DateTimeOffset itemEndTimeWithFiller = maybePlayoutItems.Max(pi => pi.FinishOffset);
|
||||||
|
|
||||||
willFinishInTime = itemStartTime > durationFinish ||
|
willFinishInTime = itemStartTime > durationFinish ||
|
||||||
itemEndTimeWithFiller <= durationFinish;
|
itemEndTimeWithFiller <= durationFinish;
|
||||||
if (willFinishInTime)
|
if (willFinishInTime)
|
||||||
{
|
{
|
||||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||||
playoutItems.AddRange(
|
playoutItems.AddRange(maybePlayoutItems);
|
||||||
AddFiller(
|
|
||||||
nextState,
|
// update original enumerators
|
||||||
collectionEnumerators,
|
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||||
scheduleItem,
|
{
|
||||||
playoutItem,
|
IMediaCollectionEnumerator clone = enumeratorClones[key];
|
||||||
itemChapters,
|
while (enumerator.State.Seed != clone.State.Seed || enumerator.State.Index != clone.State.Index)
|
||||||
cancellationToken));
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nextState = nextState with
|
nextState = nextState with
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,12 +74,23 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
|||||||
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
|
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
|
||||||
: DateTimeOffset.MaxValue;
|
: DateTimeOffset.MaxValue;
|
||||||
|
|
||||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
var enumeratorClones = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||||
collectionEnumerators,
|
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||||
|
{
|
||||||
|
IMediaCollectionEnumerator clone = enumerator.Clone(enumerator.State.Clone(), cancellationToken);
|
||||||
|
enumeratorClones.Add(key, clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlayoutItem> maybePlayoutItems = AddFiller(
|
||||||
|
nextState,
|
||||||
|
enumeratorClones,
|
||||||
scheduleItem,
|
scheduleItem,
|
||||||
itemStartTime,
|
playoutItem,
|
||||||
itemDuration,
|
itemChapters,
|
||||||
itemChapters);
|
log: false,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
DateTimeOffset itemEndTimeWithFiller = maybePlayoutItems.Max(pi => pi.FinishOffset);
|
||||||
|
|
||||||
// if the next schedule item is supposed to start during this item,
|
// if the next schedule item is supposed to start during this item,
|
||||||
// don't schedule this item and just move on
|
// don't schedule this item and just move on
|
||||||
@@ -88,27 +99,16 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
|||||||
|
|
||||||
if (willFinishInTime)
|
if (willFinishInTime)
|
||||||
{
|
{
|
||||||
playoutItems.AddRange(
|
playoutItems.AddRange(maybePlayoutItems);
|
||||||
AddFiller(
|
|
||||||
nextState,
|
|
||||||
collectionEnumerators,
|
|
||||||
scheduleItem,
|
|
||||||
playoutItem,
|
|
||||||
itemChapters,
|
|
||||||
cancellationToken));
|
|
||||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||||
|
|
||||||
if (playoutItems.Count > 0)
|
// update original enumerators
|
||||||
|
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||||
{
|
{
|
||||||
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
|
IMediaCollectionEnumerator clone = enumeratorClones[key];
|
||||||
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
|
while (enumerator.State.Seed != clone.State.Seed || enumerator.State.Index != clone.State.Index)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
enumerator.MoveNext();
|
||||||
"Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}",
|
|
||||||
itemEndTimeWithFiller,
|
|
||||||
actualEndTime);
|
|
||||||
|
|
||||||
// _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,12 +77,6 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
};
|
};
|
||||||
|
|
||||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
|
||||||
collectionEnumerators,
|
|
||||||
scheduleItem,
|
|
||||||
itemStartTime,
|
|
||||||
itemDuration,
|
|
||||||
itemChapters);
|
|
||||||
|
|
||||||
playoutItems.AddRange(
|
playoutItems.AddRange(
|
||||||
AddFiller(
|
AddFiller(
|
||||||
@@ -91,11 +85,12 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
scheduleItem,
|
scheduleItem,
|
||||||
playoutItem,
|
playoutItem,
|
||||||
itemChapters,
|
itemChapters,
|
||||||
|
log: true,
|
||||||
cancellationToken));
|
cancellationToken));
|
||||||
|
|
||||||
nextState = nextState with
|
nextState = nextState with
|
||||||
{
|
{
|
||||||
CurrentTime = itemEndTimeWithFiller,
|
CurrentTime = playoutItems.Max(pi => pi.FinishOffset),
|
||||||
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1),
|
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1),
|
||||||
|
|
||||||
// only bump guide group if we don't have a custom title
|
// only bump guide group if we don't have a custom title
|
||||||
|
|||||||
@@ -53,24 +53,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
|
|||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|
||||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
|
||||||
collectionEnumerators,
|
|
||||||
scheduleItem,
|
|
||||||
itemStartTime,
|
|
||||||
itemDuration,
|
|
||||||
itemChapters);
|
|
||||||
|
|
||||||
List<PlayoutItem> playoutItems = AddFiller(
|
List<PlayoutItem> playoutItems = AddFiller(
|
||||||
playoutBuilderState,
|
playoutBuilderState,
|
||||||
collectionEnumerators,
|
collectionEnumerators,
|
||||||
scheduleItem,
|
scheduleItem,
|
||||||
playoutItem,
|
playoutItem,
|
||||||
itemChapters,
|
itemChapters,
|
||||||
|
log: true,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
PlayoutBuilderState nextState = playoutBuilderState with
|
PlayoutBuilderState nextState = playoutBuilderState with
|
||||||
{
|
{
|
||||||
CurrentTime = itemEndTimeWithFiller
|
CurrentTime = playoutItems.Max(pi => pi.FinishOffset)
|
||||||
};
|
};
|
||||||
|
|
||||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new RandomizedMediaCollectionEnumerator(_mediaItems, state);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;
|
public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;
|
||||||
@@ -36,8 +41,7 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
State.Index++;
|
State.Index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset) =>
|
|
||||||
throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _mediaItems.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,18 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new SeasonEpisodeMediaCollectionEnumerator(_sortedMediaItems, state);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||||
|
|
||||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset) =>
|
|
||||||
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
|
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _sortedMediaItems.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new ShuffleInOrderCollectionEnumerator(_collections, state, _randomStartPoint, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||||
@@ -69,8 +74,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
State.Index %= _shuffled.Count;
|
State.Index %= _shuffled.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
private IList<MediaItem> Shuffle(IList<CollectionWithItems> collections, Random random)
|
private IList<MediaItem> Shuffle(IList<CollectionWithItems> collections, Random random)
|
||||||
{
|
{
|
||||||
// based on https://keyj.emphy.de/balanced-shuffle/
|
// based on https://keyj.emphy.de/balanced-shuffle/
|
||||||
@@ -208,4 +211,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _shuffled.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new ShuffledMediaCollectionEnumerator(_mediaItems, state, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||||
@@ -66,34 +71,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
State.Index %= _mediaItemCount;
|
State.Index %= _mediaItemCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset)
|
|
||||||
{
|
|
||||||
if (offset == 0)
|
|
||||||
{
|
|
||||||
return Current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((State.Index + offset) % _mediaItemCount == 0)
|
|
||||||
{
|
|
||||||
IList<MediaItem> shuffled;
|
|
||||||
Option<MediaItem> tail = Current;
|
|
||||||
|
|
||||||
// clone the random
|
|
||||||
CloneableRandom randomCopy = _random.Clone();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int newSeed = randomCopy.Next();
|
|
||||||
randomCopy = new CloneableRandom(newSeed);
|
|
||||||
shuffled = Shuffle(_mediaItems, randomCopy);
|
|
||||||
} while (_mediaItems.Count > 1 && shuffled[0] == tail);
|
|
||||||
|
|
||||||
return shuffled.Any() ? shuffled[0] : None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, CloneableRandom random)
|
private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, CloneableRandom random)
|
||||||
{
|
{
|
||||||
GroupedMediaItem[] copy = list.ToArray();
|
GroupedMediaItem[] copy = list.ToArray();
|
||||||
@@ -110,4 +87,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _shuffled.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace ErsatzTV.Infrastructure.Scheduling;
|
|||||||
public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerator
|
public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerator
|
||||||
{
|
{
|
||||||
private readonly CancellationToken _cancellationToken;
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
private readonly IScriptEngine _scriptEngine;
|
||||||
|
private readonly string _scriptFile;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly int _mediaItemCount;
|
private readonly int _mediaItemCount;
|
||||||
private readonly Dictionary<int, List<MediaItem>> _mediaItemGroups;
|
private readonly Dictionary<int, List<MediaItem>> _mediaItemGroups;
|
||||||
@@ -26,6 +28,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
|||||||
ILogger logger,
|
ILogger logger,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_scriptEngine = scriptEngine;
|
||||||
|
_scriptFile = scriptFile;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cancellationToken = cancellationToken;
|
_cancellationToken = cancellationToken;
|
||||||
|
|
||||||
@@ -83,6 +87,17 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new MultiEpisodeShuffleCollectionEnumerator(
|
||||||
|
_ungrouped,
|
||||||
|
state,
|
||||||
|
_scriptEngine,
|
||||||
|
_scriptFile,
|
||||||
|
_logger,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public CollectionEnumeratorState State { get; }
|
public CollectionEnumeratorState State { get; }
|
||||||
|
|
||||||
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||||
@@ -110,34 +125,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
|||||||
State.Index %= _mediaItemCount;
|
State.Index %= _mediaItemCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option<MediaItem> Peek(int offset)
|
|
||||||
{
|
|
||||||
if (offset == 0)
|
|
||||||
{
|
|
||||||
return Current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((State.Index + offset) % _mediaItemCount == 0)
|
|
||||||
{
|
|
||||||
IList<MediaItem> shuffled;
|
|
||||||
Option<MediaItem> tail = Current;
|
|
||||||
|
|
||||||
// clone the random
|
|
||||||
CloneableRandom randomCopy = _random.Clone();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int newSeed = randomCopy.Next();
|
|
||||||
randomCopy = new CloneableRandom(newSeed);
|
|
||||||
shuffled = Shuffle(randomCopy);
|
|
||||||
} while (_mediaItemCount > 1 && shuffled[0] == tail);
|
|
||||||
|
|
||||||
return shuffled.Any() ? shuffled[0] : None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IList<MediaItem> Shuffle(CloneableRandom random)
|
private IList<MediaItem> Shuffle(CloneableRandom random)
|
||||||
{
|
{
|
||||||
int maxGroupNumber = _mediaItemGroups.Max(a => a.Key);
|
int maxGroupNumber = _mediaItemGroups.Max(a => a.Key);
|
||||||
@@ -202,4 +189,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||||
|
|
||||||
|
public int Count => _shuffled.Count;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user