Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

301 lines
11 KiB

using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.BlockScheduling;
public static class BlockPlayoutEnumerator
{
public static IMediaCollectionEnumerator Chronological(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey,
ILogger logger)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = 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,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey,
ILogger logger)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = 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,
int playoutSeed,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = 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 = playoutSeed + blockItem.BlockId, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
state.Index = h.Index + 1;
}
// TODO: fix multi-collection groups, keep multi-part episodes together
var mediaItems = collectionItems
.Map(mi => new GroupedMediaItem(mi, null))
.ToList();
// it shouldn't matter which order the remaining items are shuffled in,
// as long as already-played items are not included
return new BlockPlayoutShuffledMediaCollectionEnumerator(mediaItems, state);
}
public static IMediaCollectionEnumerator Shuffle(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
int playoutSeed,
List<PlayoutHistory> playoutHistory,
int seedOffset,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
var state = new CollectionEnumeratorState { Seed = playoutSeed + seedOffset, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
state.Index = h.Index + 1;
}
// TODO: fix multi-collection groups, keep multi-part episodes together
var mediaItems = collectionItems
.Map(mi => new GroupedMediaItem(mi, null))
.ToList();
// it shouldn't matter which order the remaining items are shuffled in,
// as long as already-played items are not included
return new BlockPlayoutShuffledMediaCollectionEnumerator(mediaItems, state);
}
public static async Task<IMediaCollectionEnumerator> PlaylistForFiller(
IMediaCollectionRepository mediaCollectionRepository,
int playlistId,
DateTimeOffset currentTime,
int playoutSeed,
IReadOnlyCollection<PlayoutHistory> playoutHistory,
int seedOffset,
string historyKey,
CancellationToken cancellationToken)
{
Dictionary<PlaylistItem, List<MediaItem>> itemMap =
await mediaCollectionRepository.GetPlaylistItemMap(playlistId, cancellationToken);
var playlistMediaItems = new Dictionary<CollectionKey, List<MediaItem>>();
foreach ((PlaylistItem playlistItem, List<MediaItem> mediaItems) in itemMap)
{
playlistMediaItems.TryAdd(CollectionKey.ForPlaylistItem(playlistItem), mediaItems);
}
var state = new CollectionEnumeratorState { Seed = playoutSeed + seedOffset, Index = 0 };
var enumerator = await PlaylistEnumerator.Create(
mediaCollectionRepository,
itemMap,
state,
shufflePlaylistItems: false,
batchSize: Option<int>.None,
randomStartPoint: false,
cancellationToken);
DateTime historyTime = currentTime.UtcDateTime;
Option<DateTime> maxWhen = await playoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.Map(h => h.When)
.OrderByDescending(h => h)
.HeadOrNone()
.IfNoneAsync(DateTime.MinValue);
var maybeHistory = playoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When == maxWhen)
.ToList();
Option<PlayoutHistory> maybePrimaryHistory = maybeHistory
.Filter(h => string.IsNullOrWhiteSpace(h.ChildKey))
.HeadOrNone();
foreach (PlayoutHistory primaryHistory in maybePrimaryHistory)
{
// the primary row holds the playlist index; a child index counts the items of one
// collection, so it does not describe a playlist position
enumerator.ResetState(
new CollectionEnumeratorState
{
Seed = primaryHistory.Seed ?? enumerator.State.Seed,
Index = primaryHistory.Index
});
foreach ((IMediaCollectionEnumerator childEnumerator, CollectionKey collectionKey) in
enumerator.ChildEnumerators)
{
PlaybackOrder itemPlaybackOrder = childEnumerator switch
{
ChronologicalMediaCollectionEnumerator => PlaybackOrder.Chronological,
RandomizedMediaCollectionEnumerator => PlaybackOrder.Random,
ShuffledMediaCollectionEnumerator => PlaybackOrder.Shuffle,
_ => PlaybackOrder.None
};
Option<PlayoutHistory> maybeApplicableHistory = maybeHistory
.Filter(h => h.ChildKey == HistoryDetails.KeyForCollectionKey(collectionKey))
.HeadOrNone();
List<MediaItem> collectionItems = playlistMediaItems[collectionKey];
if (collectionItems.Count == 0)
{
continue;
}
foreach (PlayoutHistory h in maybeApplicableHistory)
{
// logger.LogDebug(
// "History is applicable: {When}: {ChildKey} / {History} / {IsCurrentChild}",
// h.When,
// h.ChildKey,
// h.Details,
// h.IsCurrentChild);
// a shuffled child gets a new seed each time it wraps.
// the replay from the cycle start cannot make that order again.
if (itemPlaybackOrder is PlaybackOrder.Shuffle)
{
childEnumerator.ResetState(
new CollectionEnumeratorState
{
Seed = h.Seed ?? childEnumerator.State.Seed,
Index = h.Index,
Started = childEnumerator.State.Started
});
}
// the collection may have changed since the last build, so the replayed
// position can point at the wrong item
if (itemPlaybackOrder is PlaybackOrder.Chronological)
{
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
childEnumerator,
itemPlaybackOrder,
true);
}
// the playlist order may have changed since the last build
if (h.IsCurrentChild)
{
enumerator.EnsureCurrentChild(collectionKey);
}
}
}
// only move next at the end, because that may also move
// the enumerator index
enumerator.MoveNext(Option<DateTimeOffset>.None);
}
return enumerator;
}
public static IMediaCollectionEnumerator RandomRotation(
List<MediaItem> collectionItems,
DateTimeOffset currentTime,
int playoutSeed,
List<PlayoutHistory> playoutHistory,
BlockItem blockItem,
string historyKey)
{
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = 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 = playoutSeed + blockItem.BlockId, Index = 0 };
foreach (PlayoutHistory h in maybeHistory)
{
// Make sure to only increase the index by 1 since we can only
// guarantee the next one is a different show. h.Index comes from
// the previous play item which already increased by 1.
state.Index = h.Index;
}
return new RandomizedRotatingMediaCollectionEnumerator(collectionItems, state);
}
}