mirror of https://github.com/ErsatzTV/ErsatzTV.git
15 changed files with 10093 additions and 180 deletions
@ -0,0 +1,67 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Extensions; |
||||||
|
using ErsatzTV.Core.Interfaces.Scheduling; |
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling.BlockScheduling; |
||||||
|
|
||||||
|
public class BlockPlayoutShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator |
||||||
|
{ |
||||||
|
private readonly IList<GroupedMediaItem> _mediaItems; |
||||||
|
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration; |
||||||
|
private readonly int _mediaItemCount; |
||||||
|
private IList<MediaItem> _shuffled; |
||||||
|
|
||||||
|
public BlockPlayoutShuffledMediaCollectionEnumerator( |
||||||
|
IList<GroupedMediaItem> mediaItems, |
||||||
|
CollectionEnumeratorState state) |
||||||
|
{ |
||||||
|
_mediaItems = mediaItems; |
||||||
|
_mediaItemCount = _mediaItems.Sum(i => 1 + Optional(i.Additional).Flatten().Count()); |
||||||
|
|
||||||
|
State = state; |
||||||
|
|
||||||
|
_shuffled = Shuffle(_mediaItems); |
||||||
|
_lazyMinimumDuration = |
||||||
|
new Lazy<Option<TimeSpan>>( |
||||||
|
() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone()); |
||||||
|
} |
||||||
|
|
||||||
|
public void ResetState(CollectionEnumeratorState state) |
||||||
|
{ |
||||||
|
// only re-shuffle if needed
|
||||||
|
if (State.Seed != state.Seed || State.Index != state.Index) |
||||||
|
{ |
||||||
|
State.Seed = state.Seed; |
||||||
|
State.Index = state.Index; |
||||||
|
|
||||||
|
_shuffled = Shuffle(_mediaItems); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public CollectionEnumeratorState State { get; } |
||||||
|
|
||||||
|
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; |
||||||
|
|
||||||
|
public void MoveNext() |
||||||
|
{ |
||||||
|
State.Index++; |
||||||
|
} |
||||||
|
|
||||||
|
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value; |
||||||
|
|
||||||
|
public int Count => _shuffled.Count; |
||||||
|
|
||||||
|
private IList<MediaItem> Shuffle(IList<GroupedMediaItem> list) |
||||||
|
{ |
||||||
|
var copy = new GroupedMediaItem[list.Count]; |
||||||
|
|
||||||
|
var superShuffle = new SuperShuffle(); |
||||||
|
for (var i = 0; i < list.Count; i++) |
||||||
|
{ |
||||||
|
int toSelect = superShuffle.Shuffle(i, State.Seed + (State.Index / list.Count), list.Count); |
||||||
|
copy[i] = list[toSelect]; |
||||||
|
} |
||||||
|
|
||||||
|
return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount); |
||||||
|
} |
||||||
|
} |
@ -1,106 +0,0 @@ |
|||||||
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); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,65 @@ |
|||||||
|
// adapted from https://github.com/RondeSC/Super_Shuffle
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Scheduling; |
||||||
|
|
||||||
|
public class SuperShuffle |
||||||
|
{ |
||||||
|
private int userSID = 1; |
||||||
|
private int si, r1, r2, r3, r4; |
||||||
|
private int randR, halfN, rx, rkey; |
||||||
|
|
||||||
|
public int Shuffle(int inx, int shuffleId, int listSize) { |
||||||
|
int si, hi, offset; |
||||||
|
int halfSize = listSize / 2; // for 1/2 the processing range
|
||||||
|
|
||||||
|
shuffleId += 131 * (inx / listSize); // have inx overflows supported
|
||||||
|
si = (inx % listSize); |
||||||
|
if (si < halfSize) { |
||||||
|
hi = si; |
||||||
|
offset = 0; |
||||||
|
} else { |
||||||
|
hi = listSize - 1 - si; |
||||||
|
offset = halfSize; |
||||||
|
halfSize = listSize - halfSize; |
||||||
|
shuffleId++; |
||||||
|
} |
||||||
|
|
||||||
|
hi = MillerShuffleE(hi, shuffleId, halfSize); // use any STD MSA() shuffle (aka: a PRIG function)
|
||||||
|
si = MillerShuffleE(hi + offset, userSID, listSize); // indexing into the baseline shuffle
|
||||||
|
return(si); |
||||||
|
} |
||||||
|
|
||||||
|
private int MillerShuffleE(int inx, int shuffleId, int listSize) |
||||||
|
{ |
||||||
|
const int p1 = 24317, p2 = 32141, p3 = 63629; // good for shuffling >60,000 indexes
|
||||||
|
|
||||||
|
shuffleId += 131 * (inx / listSize); // have inx overflow effect the mix
|
||||||
|
si = (inx + shuffleId) % listSize; // cut the deck
|
||||||
|
|
||||||
|
randR = shuffleId; //local randomizer
|
||||||
|
r1 = randR % p3; |
||||||
|
r2 = randR % p1; // Now, per Chinese remainder theorem, (r1,r2,r3) will be a unique set
|
||||||
|
r3 = randR % p2; |
||||||
|
r4 = randR % 2749; |
||||||
|
halfN = listSize / 2 + 1; |
||||||
|
rx = (randR / listSize) % listSize + 1; |
||||||
|
rkey = (randR / listSize / listSize) % listSize + 1; |
||||||
|
|
||||||
|
// perform the conditional multi-faceted mathematical mixing (on avg 2 5/6 shuffle ops done + 2 simple Xors)
|
||||||
|
if (si % 3 == 0) si = ((si / 3 * p1 + r1) % ((listSize + 2) / 3)) * 3; // spin multiples of 3
|
||||||
|
if (si <= halfN) |
||||||
|
{ |
||||||
|
si = (si + r3) % (halfN + 1); |
||||||
|
si = halfN - si; |
||||||
|
} // improves large permu distro
|
||||||
|
|
||||||
|
if (si % 2 == 0) si = ((si / 2 * p2 + r2) % ((listSize + 1) / 2)) * 2; // spin multiples of 2
|
||||||
|
if (si < halfN) si = (si * p3 + r3) % halfN; |
||||||
|
|
||||||
|
if ((si ^ rx) < listSize) si ^= rx; // flip some bits with Xor
|
||||||
|
si = (si * p3 + r4) % listSize; // a relatively prime gears churning operation
|
||||||
|
if ((si ^ rkey) < listSize) si ^= rkey; |
||||||
|
|
||||||
|
return si; |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,39 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
#nullable disable |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_PlayoutSeed : Migration |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.RenameColumn( |
||||||
|
name: "Seed", |
||||||
|
table: "PlayoutHistory", |
||||||
|
newName: "Index"); |
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "Seed", |
||||||
|
table: "Playout", |
||||||
|
type: "int", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "Seed", |
||||||
|
table: "Playout"); |
||||||
|
|
||||||
|
migrationBuilder.RenameColumn( |
||||||
|
name: "Index", |
||||||
|
table: "PlayoutHistory", |
||||||
|
newName: "Seed"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,39 @@ |
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations; |
||||||
|
|
||||||
|
#nullable disable |
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Add_PlayoutSeed : Migration |
||||||
|
{ |
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.RenameColumn( |
||||||
|
name: "Seed", |
||||||
|
table: "PlayoutHistory", |
||||||
|
newName: "Index"); |
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>( |
||||||
|
name: "Seed", |
||||||
|
table: "Playout", |
||||||
|
type: "INTEGER", |
||||||
|
nullable: false, |
||||||
|
defaultValue: 0); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) |
||||||
|
{ |
||||||
|
migrationBuilder.DropColumn( |
||||||
|
name: "Seed", |
||||||
|
table: "Playout"); |
||||||
|
|
||||||
|
migrationBuilder.RenameColumn( |
||||||
|
name: "Index", |
||||||
|
table: "PlayoutHistory", |
||||||
|
newName: "Seed"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue