allow shuffling block items (#1571)
* allow shuffling block items * fix drop down search results
This commit is contained in:
@@ -36,6 +36,7 @@ public class BlockPlayoutBuilder(
|
||||
[
|
||||
PlaybackOrder.Chronological,
|
||||
PlaybackOrder.SeasonEpisode,
|
||||
PlaybackOrder.Shuffle,
|
||||
PlaybackOrder.Random
|
||||
];
|
||||
|
||||
@@ -177,6 +178,8 @@ public class BlockPlayoutBuilder(
|
||||
{
|
||||
PlayoutId = playout.Id,
|
||||
BlockId = blockItem.BlockId,
|
||||
PlaybackOrder = blockItem.PlaybackOrder,
|
||||
Seed = enumerator.State.Seed,
|
||||
When = currentTime.UtcDateTime,
|
||||
Key = historyKey,
|
||||
Details = HistoryDetails.ForMediaItem(mediaItem)
|
||||
@@ -212,41 +215,37 @@ public class BlockPlayoutBuilder(
|
||||
string historyKey,
|
||||
Map<CollectionKey, List<MediaItem>> collectionMediaItems)
|
||||
{
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.HeadOrNone();
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
|
||||
|
||||
var collectionKey = CollectionKey.ForBlockItem(blockItem);
|
||||
List<MediaItem> collectionItems = collectionMediaItems[collectionKey];
|
||||
|
||||
// get enumerator
|
||||
IMediaCollectionEnumerator enumerator = blockItem.PlaybackOrder switch
|
||||
{
|
||||
PlaybackOrder.Chronological => new ChronologicalMediaCollectionEnumerator(collectionItems, state),
|
||||
PlaybackOrder.SeasonEpisode => new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state),
|
||||
PlaybackOrder.Chronological => BlockPlayoutEnumerator.Chronological(
|
||||
collectionItems,
|
||||
currentTime,
|
||||
playout,
|
||||
blockItem,
|
||||
historyKey,
|
||||
Logger),
|
||||
PlaybackOrder.SeasonEpisode => BlockPlayoutEnumerator.SeasonEpisode(
|
||||
collectionItems,
|
||||
currentTime,
|
||||
playout,
|
||||
blockItem,
|
||||
historyKey,
|
||||
Logger),
|
||||
PlaybackOrder.Shuffle => BlockPlayoutEnumerator.Shuffle(
|
||||
collectionItems,
|
||||
currentTime,
|
||||
playout,
|
||||
blockItem,
|
||||
historyKey),
|
||||
_ => new RandomizedMediaCollectionEnumerator(
|
||||
collectionItems,
|
||||
new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 })
|
||||
};
|
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory history in maybeHistory)
|
||||
{
|
||||
Logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details);
|
||||
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
history.Details,
|
||||
enumerator,
|
||||
blockItem.PlaybackOrder);
|
||||
}
|
||||
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
@@ -288,8 +287,20 @@ public class BlockPlayoutBuilder(
|
||||
|
||||
IEnumerable<PlayoutHistory> toDelete = group
|
||||
.Filter(h => h.When < start.UtcDateTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.Tail();
|
||||
.OrderByDescending(h => h.When);
|
||||
|
||||
// chronological and season, episode only need to keep most recent entry
|
||||
if (group.Count > 0 && group[0].PlaybackOrder is PlaybackOrder.Chronological or PlaybackOrder.SeasonEpisode)
|
||||
{
|
||||
toDelete = toDelete.Tail();
|
||||
}
|
||||
|
||||
// shuffle needs to keep all entries with current seed
|
||||
if (group.Count > 0 && group[0].PlaybackOrder is PlaybackOrder.Shuffle)
|
||||
{
|
||||
int currentSeed = group[0].Seed;
|
||||
toDelete = toDelete.Filter(h => h.Seed != currentSeed);
|
||||
}
|
||||
|
||||
foreach (PlayoutHistory delete in toDelete)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Collections.Immutable;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
|
||||
public static class BlockPlayoutEnumerator
|
||||
{
|
||||
public static IMediaCollectionEnumerator Chronological(
|
||||
List<MediaItem> collectionItems,
|
||||
DateTimeOffset currentTime,
|
||||
Playout playout,
|
||||
BlockItem blockItem,
|
||||
string historyKey,
|
||||
ILogger logger)
|
||||
{
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.HeadOrNone();
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(collectionItems, state);
|
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory h in maybeHistory)
|
||||
{
|
||||
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
|
||||
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
h.Details,
|
||||
enumerator,
|
||||
blockItem.PlaybackOrder);
|
||||
}
|
||||
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
public static IMediaCollectionEnumerator SeasonEpisode(
|
||||
List<MediaItem> collectionItems,
|
||||
DateTimeOffset currentTime,
|
||||
Playout playout,
|
||||
BlockItem blockItem,
|
||||
string historyKey,
|
||||
ILogger logger)
|
||||
{
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.HeadOrNone();
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
|
||||
|
||||
var enumerator = new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state);
|
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory h in maybeHistory)
|
||||
{
|
||||
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
|
||||
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
h.Details,
|
||||
enumerator,
|
||||
blockItem.PlaybackOrder);
|
||||
}
|
||||
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
public static IMediaCollectionEnumerator Shuffle(
|
||||
List<MediaItem> collectionItems,
|
||||
DateTimeOffset currentTime,
|
||||
Playout playout,
|
||||
BlockItem blockItem,
|
||||
string historyKey)
|
||||
{
|
||||
// need a new shuffled media collection enumerator that can "hide" items for one iteration, then include all items again
|
||||
// maybe take a "masked items" hash set, then clear it after shuffling
|
||||
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
var maskedMediaItemIds = new System.Collections.Generic.HashSet<int>();
|
||||
List<PlayoutHistory> history = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.ToList();
|
||||
|
||||
if (history.Count > 0)
|
||||
{
|
||||
int currentSeed = history[0].Seed;
|
||||
history = history.Filter(h => h.Seed == currentSeed).ToList();
|
||||
}
|
||||
|
||||
var knownMediaIds = collectionItems.Map(ci => ci.Id).ToImmutableHashSet();
|
||||
foreach (PlayoutHistory h in history)
|
||||
{
|
||||
HistoryDetails.Details details = JsonConvert.DeserializeObject<HistoryDetails.Details>(h.Details);
|
||||
foreach (int mediaItemId in Optional(details.MediaItemId))
|
||||
{
|
||||
if (knownMediaIds.Contains(mediaItemId))
|
||||
{
|
||||
maskedMediaItemIds.Add(mediaItemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 };
|
||||
|
||||
// keep the current seed if one exists
|
||||
if (maskedMediaItemIds.Count > 0 && maskedMediaItemIds.Count < collectionItems.Count && history.Count > 0)
|
||||
{
|
||||
state.Seed = history[0].Seed;
|
||||
}
|
||||
|
||||
// if everything is masked, nothing is masked
|
||||
if (maskedMediaItemIds.Count == collectionItems.Count)
|
||||
{
|
||||
maskedMediaItemIds.Clear();
|
||||
}
|
||||
|
||||
// TODO: fix multi-collection groups, keep multi-part episodes together
|
||||
var mediaItems = collectionItems
|
||||
.Map(mi => new GroupedMediaItem(mi, null))
|
||||
.ToList();
|
||||
|
||||
Serilog.Log.Logger.Debug(
|
||||
"scheduling {X} media items with {Y} masked",
|
||||
mediaItems.Count,
|
||||
maskedMediaItemIds.Count);
|
||||
|
||||
// it shouldn't matter which order the remaining items are shuffled in,
|
||||
// as long as already-played items are not included
|
||||
return new MaskedShuffledMediaCollectionEnumerator(
|
||||
mediaItems,
|
||||
maskedMediaItemIds,
|
||||
state,
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
public class MaskedShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
private readonly int _mediaItemCount;
|
||||
private readonly IList<GroupedMediaItem> _mediaItems;
|
||||
private CloneableRandom _random;
|
||||
private IList<MediaItem> _shuffled;
|
||||
|
||||
public MaskedShuffledMediaCollectionEnumerator(
|
||||
IList<GroupedMediaItem> mediaItems,
|
||||
IReadOnlySet<int> maskedMediaItemIds,
|
||||
CollectionEnumeratorState state,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_mediaItemCount = mediaItems.Sum(i => 1 + Optional(i.Additional).Flatten().Count());
|
||||
_mediaItems = mediaItems;
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
if (state.Index >= _mediaItems.Count)
|
||||
{
|
||||
state.Index = 0;
|
||||
state.Seed = new Random(state.Seed).Next();
|
||||
}
|
||||
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
|
||||
// remove masked items from initial shuffle
|
||||
var filtered = _mediaItems.Filter(mi => !maskedMediaItemIds.Contains(mi.First.Id)).ToList();
|
||||
foreach (GroupedMediaItem group in filtered)
|
||||
{
|
||||
group.Additional.RemoveAll(mi => maskedMediaItemIds.Contains(mi.Id));
|
||||
}
|
||||
|
||||
_shuffled = Shuffle(filtered, _random);
|
||||
_lazyMinimumDuration =
|
||||
new Lazy<Option<TimeSpan>>(
|
||||
() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
|
||||
State = state;
|
||||
}
|
||||
|
||||
public void ResetState(CollectionEnumeratorState state)
|
||||
{
|
||||
// only re-shuffle if needed
|
||||
if (State.Seed != state.Seed)
|
||||
{
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_mediaItems, _random);
|
||||
}
|
||||
|
||||
State.Index = state.Index;
|
||||
}
|
||||
|
||||
public CollectionEnumeratorState State { get; }
|
||||
|
||||
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||
|
||||
public void MoveNext()
|
||||
{
|
||||
if ((State.Index + 1) % _mediaItemCount == 0)
|
||||
{
|
||||
Option<MediaItem> tail = Current;
|
||||
|
||||
State.Index = 0;
|
||||
do
|
||||
{
|
||||
State.Seed = _random.Next();
|
||||
_random = new CloneableRandom(State.Seed);
|
||||
_shuffled = Shuffle(_mediaItems, _random);
|
||||
} while (!_cancellationToken.IsCancellationRequested && _mediaItems.Count > 1 &&
|
||||
Current.Map(x => x.Id) == tail.Map(x => x.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
State.Index++;
|
||||
}
|
||||
|
||||
State.Index %= _mediaItemCount;
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
|
||||
public int Count => _shuffled.Count;
|
||||
|
||||
private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, CloneableRandom random)
|
||||
{
|
||||
GroupedMediaItem[] copy = list.ToArray();
|
||||
|
||||
int n = copy.Length;
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
int k = random.Next(n + 1);
|
||||
(copy[k], copy[n]) = (copy[n], copy[k]);
|
||||
}
|
||||
|
||||
return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user