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
|
||||
- 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
|
||||
### Added
|
||||
- Synchronize actor metadata from Jellyfin and Emby television libraries
|
||||
|
||||
@@ -23,189 +23,6 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
private CancellationToken _cancellationToken;
|
||||
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]
|
||||
public class AddFiller : PlayoutModeSchedulerBaseTests
|
||||
{
|
||||
@@ -241,6 +58,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
scheduleItem,
|
||||
new PlayoutItem(),
|
||||
new List<MediaChapter>(),
|
||||
log: true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
@@ -293,6 +111,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
scheduleItem,
|
||||
new PlayoutItem(),
|
||||
new List<MediaChapter> { new() },
|
||||
log: true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
@@ -359,6 +178,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) }
|
||||
},
|
||||
log: true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
@@ -450,6 +270,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
|
||||
},
|
||||
log: true,
|
||||
_cancellationToken);
|
||||
|
||||
playoutItems.Count.Should().Be(5);
|
||||
@@ -555,6 +376,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
|
||||
},
|
||||
log: true,
|
||||
_cancellationToken);
|
||||
|
||||
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 Index { get; set; }
|
||||
public CollectionEnumeratorState Clone() => new CollectionEnumeratorState { Seed = Seed, Index = Index };
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
public interface IMediaCollectionEnumerator
|
||||
{
|
||||
IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken);
|
||||
CollectionEnumeratorState State { get; }
|
||||
Option<MediaItem> Current { get; }
|
||||
void MoveNext();
|
||||
Option<MediaItem> Peek(int offset);
|
||||
int Count { 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 Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||
|
||||
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 int Count => _sortedMediaItems.Count;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly Collection _collection;
|
||||
private readonly IList<MediaItem> _mediaItems;
|
||||
|
||||
private readonly IList<MediaItem> _sortedMediaItems;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
|
||||
@@ -14,6 +17,9 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
IList<MediaItem> mediaItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_collection = collection;
|
||||
_mediaItems = mediaItems;
|
||||
|
||||
// TODO: this will break if we allow shows and seasons
|
||||
_sortedMediaItems = collection.CollectionItems
|
||||
.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 Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||
|
||||
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 int Count => _sortedMediaItems.Count;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
@@ -195,142 +197,13 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
PlayoutBuilder.DisplayTitle(mediaItem),
|
||||
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(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
PlayoutItem playoutItem,
|
||||
List<MediaChapter> chapters,
|
||||
bool log,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<PlayoutItem>();
|
||||
@@ -367,6 +240,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
filler.Duration.Value,
|
||||
FillerKind.PreRoll,
|
||||
filler.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
@@ -408,6 +282,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
filler.Duration.Value,
|
||||
FillerKind.MidRoll,
|
||||
filler.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -450,6 +325,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
filler.Duration.Value,
|
||||
FillerKind.PostRoll,
|
||||
filler.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
@@ -518,6 +394,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
remainingToFill,
|
||||
FillerKind.PreRoll,
|
||||
padFiller.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
totalDuration =
|
||||
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
||||
@@ -544,6 +421,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
remainingToFill,
|
||||
FillerKind.MidRoll,
|
||||
padFiller.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
TimeSpan average = effectiveChapters.Count <= 1
|
||||
? remainingToFill
|
||||
@@ -607,6 +485,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
remainingToFill,
|
||||
FillerKind.PostRoll,
|
||||
padFiller.AllowWatermarks,
|
||||
log,
|
||||
cancellationToken));
|
||||
totalDuration =
|
||||
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
||||
@@ -682,13 +561,15 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
TimeSpan duration,
|
||||
FillerKind fillerKind,
|
||||
bool allowWatermarks,
|
||||
bool log,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<PlayoutItem>();
|
||||
|
||||
TimeSpan remainingToFill = duration;
|
||||
var skipped = false;
|
||||
while (enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero)
|
||||
var discardToFillAttempts = 0;
|
||||
while (enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero &&
|
||||
remainingToFill >= enumerator.MinimumDuration)
|
||||
{
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
{
|
||||
@@ -712,36 +593,26 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
result.Add(playoutItem);
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
else if (skipped)
|
||||
else
|
||||
{
|
||||
discardToFillAttempts++;
|
||||
if (discardToFillAttempts >= enumerator.Count)
|
||||
{
|
||||
// set to zero so it breaks out of the while loop
|
||||
remainingToFill = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (itemDuration >= duration * 1.5)
|
||||
if (log)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Filler item is too long {FillerDuration} to fill {GapDuration}; skipping to next filler item",
|
||||
_logger.LogDebug(
|
||||
"Filler item is too long {FillerDuration:g} to fill {GapDuration:g}; skipping to next filler item",
|
||||
itemDuration,
|
||||
duration);
|
||||
remainingToFill);
|
||||
}
|
||||
|
||||
skipped = true;
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (itemDuration > duration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Filler item is too long {FillerDuration} to fill {GapDuration}; aborting filler block",
|
||||
itemDuration,
|
||||
duration);
|
||||
}
|
||||
|
||||
// set to zero so it breaks out of the while loop
|
||||
remainingToFill = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,25 +121,41 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||
|
||||
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,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
log: false,
|
||||
cancellationToken);
|
||||
|
||||
DateTimeOffset itemEndTimeWithFiller = maybePlayoutItems.Max(pi => pi.FinishOffset);
|
||||
|
||||
willFinishInTime = itemStartTime > durationFinish ||
|
||||
itemEndTimeWithFiller <= durationFinish;
|
||||
if (willFinishInTime)
|
||||
{
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
playoutItems.AddRange(
|
||||
AddFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
cancellationToken));
|
||||
playoutItems.AddRange(maybePlayoutItems);
|
||||
|
||||
// update original enumerators
|
||||
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
|
||||
{
|
||||
IMediaCollectionEnumerator clone = enumeratorClones[key];
|
||||
while (enumerator.State.Seed != clone.State.Seed || enumerator.State.Index != clone.State.Index)
|
||||
{
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
|
||||
@@ -74,12 +74,23 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
||||
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
|
||||
: DateTimeOffset.MaxValue;
|
||||
|
||||
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,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
log: false,
|
||||
cancellationToken);
|
||||
|
||||
DateTimeOffset itemEndTimeWithFiller = maybePlayoutItems.Max(pi => pi.FinishOffset);
|
||||
|
||||
// if the next schedule item is supposed to start during this item,
|
||||
// don't schedule this item and just move on
|
||||
@@ -88,27 +99,16 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
||||
|
||||
if (willFinishInTime)
|
||||
{
|
||||
playoutItems.AddRange(
|
||||
AddFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
cancellationToken));
|
||||
playoutItems.AddRange(maybePlayoutItems);
|
||||
// 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);
|
||||
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
|
||||
IMediaCollectionEnumerator clone = enumeratorClones[key];
|
||||
while (enumerator.State.Seed != clone.State.Seed || enumerator.State.Index != clone.State.Index)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}",
|
||||
itemEndTimeWithFiller,
|
||||
actualEndTime);
|
||||
|
||||
// _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems);
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,12 +77,6 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
||||
};
|
||||
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
|
||||
playoutItems.AddRange(
|
||||
AddFiller(
|
||||
@@ -91,11 +85,12 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
log: true,
|
||||
cancellationToken));
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
CurrentTime = playoutItems.Max(pi => pi.FinishOffset),
|
||||
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1),
|
||||
|
||||
// only bump guide group if we don't have a custom title
|
||||
|
||||
@@ -53,24 +53,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
|
||||
List<PlayoutItem> playoutItems = AddFiller(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
log: true,
|
||||
cancellationToken);
|
||||
|
||||
PlayoutBuilderState nextState = playoutBuilderState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller
|
||||
CurrentTime = playoutItems.Max(pi => pi.FinishOffset)
|
||||
};
|
||||
|
||||
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 Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;
|
||||
@@ -36,8 +41,7 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
State.Index++;
|
||||
}
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
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 Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||
|
||||
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 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 Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||
@@ -69,8 +74,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
State.Index %= _shuffled.Count;
|
||||
}
|
||||
|
||||
public Option<MediaItem> Peek(int offset) => throw new NotSupportedException();
|
||||
|
||||
private IList<MediaItem> Shuffle(IList<CollectionWithItems> collections, Random random)
|
||||
{
|
||||
// based on https://keyj.emphy.de/balanced-shuffle/
|
||||
@@ -208,4 +211,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
}
|
||||
|
||||
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 Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||
@@ -66,34 +71,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
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)
|
||||
{
|
||||
GroupedMediaItem[] copy = list.ToArray();
|
||||
@@ -110,4 +87,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
|
||||
public int Count => _shuffled.Count;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace ErsatzTV.Infrastructure.Scheduling;
|
||||
public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly IScriptEngine _scriptEngine;
|
||||
private readonly string _scriptFile;
|
||||
private readonly ILogger _logger;
|
||||
private readonly int _mediaItemCount;
|
||||
private readonly Dictionary<int, List<MediaItem>> _mediaItemGroups;
|
||||
@@ -26,6 +28,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
ILogger logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_scriptEngine = scriptEngine;
|
||||
_scriptFile = scriptFile;
|
||||
_logger = logger;
|
||||
_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 Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||
@@ -110,34 +125,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
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)
|
||||
{
|
||||
int maxGroupNumber = _mediaItemGroups.Max(a => a.Key);
|
||||
@@ -202,4 +189,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
|
||||
public int Count => _shuffled.Count;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user