mirror of https://github.com/ErsatzTV/ErsatzTV.git
15 changed files with 10093 additions and 180 deletions
@ -0,0 +1,67 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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