Browse Source

add option to keep multi-part episodes together when shuffling (#188)

* add setting to keep multi-part episodes together

* keep multi-part episodes together when shuffling
pull/189/head
Jason Dove 4 years ago committed by GitHub
parent
commit
47e9a319ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs
  2. 6
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs
  3. 3
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs
  4. 3
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs
  5. 6
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  6. 6
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs
  7. 2
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  8. 10
      ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs
  9. 2
      ErsatzTV.Core/Domain/MediaItem/Episode.cs
  10. 5
      ErsatzTV.Core/Domain/MediaItem/JellyfinEpisode.cs
  11. 1
      ErsatzTV.Core/Domain/ProgramSchedule.cs
  12. 6
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  13. 85
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  14. 86
      ErsatzTV.Core/Scheduling/ChronologicalMediaComparer.cs
  15. 7
      ErsatzTV.Core/Scheduling/GroupedMediaItem.cs
  16. 87
      ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs
  17. 5
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  18. 26
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  19. 2474
      ErsatzTV.Infrastructure/Migrations/20210517232825_Add_ProgramSchedule_KeepMultiPartEpisodesTogether.Designer.cs
  20. 20
      ErsatzTV.Infrastructure/Migrations/20210517232825_Add_ProgramSchedule_KeepMultiPartEpisodesTogether.cs
  21. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  22. 7
      ErsatzTV/Pages/ScheduleEditor.razor
  23. 9
      ErsatzTV/ViewModels/ProgramScheduleEditViewModel.cs

6
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs

@ -5,6 +5,8 @@ using MediatR;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public record CreateProgramSchedule(string Name, PlaybackOrder MediaCollectionPlaybackOrder) : public record CreateProgramSchedule(
IRequest<Either<BaseError, ProgramScheduleViewModel>>; string Name,
PlaybackOrder MediaCollectionPlaybackOrder,
bool KeepMultiPartEpisodesTogether) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
} }

6
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs

@ -36,7 +36,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
.MapT( .MapT(
name => new ProgramSchedule name => new ProgramSchedule
{ {
Name = name, MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder Name = name,
MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder,
KeepMultiPartEpisodesTogether =
request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
request.KeepMultiPartEpisodesTogether
}); });
private async Task<Validation<BaseError, string>> ValidateName(CreateProgramSchedule createProgramSchedule) private async Task<Validation<BaseError, string>> ValidateName(CreateProgramSchedule createProgramSchedule)

3
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs

@ -9,5 +9,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
( (
int ProgramScheduleId, int ProgramScheduleId,
string Name, string Name,
PlaybackOrder MediaCollectionPlaybackOrder) : IRequest<Either<BaseError, ProgramScheduleViewModel>>; PlaybackOrder MediaCollectionPlaybackOrder,
bool KeepMultiPartEpisodesTogether) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
} }

3
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs

@ -43,6 +43,9 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
programSchedule.Name = update.Name; programSchedule.Name = update.Name;
programSchedule.MediaCollectionPlaybackOrder = update.MediaCollectionPlaybackOrder; programSchedule.MediaCollectionPlaybackOrder = update.MediaCollectionPlaybackOrder;
programSchedule.KeepMultiPartEpisodesTogether =
update.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
update.KeepMultiPartEpisodesTogether;
await _programScheduleRepository.Update(programSchedule); await _programScheduleRepository.Update(programSchedule);
if (needToRebuildPlayout) if (needToRebuildPlayout)

6
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -6,7 +6,11 @@ namespace ErsatzTV.Application.ProgramSchedules
internal static class Mapper internal static class Mapper
{ {
internal static ProgramScheduleViewModel ProjectToViewModel(ProgramSchedule programSchedule) => internal static ProgramScheduleViewModel ProjectToViewModel(ProgramSchedule programSchedule) =>
new(programSchedule.Id, programSchedule.Name, programSchedule.MediaCollectionPlaybackOrder); new(
programSchedule.Id,
programSchedule.Name,
programSchedule.MediaCollectionPlaybackOrder,
programSchedule.KeepMultiPartEpisodesTogether);
internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) => internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) =>
programScheduleItem switch programScheduleItem switch

6
ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs

@ -2,5 +2,9 @@
namespace ErsatzTV.Application.ProgramSchedules namespace ErsatzTV.Application.ProgramSchedules
{ {
public record ProgramScheduleViewModel(int Id, string Name, PlaybackOrder MediaCollectionPlaybackOrder); public record ProgramScheduleViewModel(
int Id,
string Name,
PlaybackOrder MediaCollectionPlaybackOrder,
bool KeepMultiPartEpisodesTogether);
} }

2
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs

@ -277,7 +277,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
} }
[Test] [Test]
public void Should_ScaleToEvenDimensions_ForTransportStream() public void Should_ScaleToEvenDimensions_ForTransportStream()
{ {

10
ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs

@ -23,7 +23,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
// normally returns 10 5 7 4 3 6 2 8 9 1 1 (note duplicate 1 at end) // normally returns 10 5 7 4 3 6 2 8 9 1 1 (note duplicate 1 at end)
var state = new CollectionEnumeratorState { Seed = 8 }; var state = new CollectionEnumeratorState { Seed = 8 };
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state); var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
var list = new List<int>(); var list = new List<int>();
for (var i = 1; i <= 1000; i++) for (var i = 1; i <= 1000; i++)
@ -50,7 +50,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
var state = new CollectionEnumeratorState(); var state = new CollectionEnumeratorState();
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state); var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
var list = new List<int>(); var list = new List<int>();
for (var i = 1; i <= 10; i++) for (var i = 1; i <= 10; i++)
@ -70,7 +70,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
var state = new CollectionEnumeratorState(); var state = new CollectionEnumeratorState();
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state); var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
var list = new List<int>(); var list = new List<int>();
for (var i = 1; i <= 10; i++) for (var i = 1; i <= 10; i++)
@ -90,7 +90,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
List<MediaItem> contents = Episodes(10); List<MediaItem> contents = Episodes(10);
var state = new CollectionEnumeratorState(); var state = new CollectionEnumeratorState();
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state); var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
List<MediaItem> contents = Episodes(10); List<MediaItem> contents = Episodes(10);
var state = new CollectionEnumeratorState { Index = 5, Seed = MagicSeed }; var state = new CollectionEnumeratorState { Index = 5, Seed = MagicSeed };
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state); var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state, false);
for (var i = 6; i <= 10; i++) for (var i = 6; i <= 10; i++)
{ {

2
ErsatzTV.Core/Domain/MediaItem/Episode.cs

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
namespace ErsatzTV.Core.Domain namespace ErsatzTV.Core.Domain
{ {
[DebuggerDisplay("{EpisodeMetadata[0].Title}")]
public class Episode : MediaItem public class Episode : MediaItem
{ {
public int EpisodeNumber { get; set; } public int EpisodeNumber { get; set; }

5
ErsatzTV.Core/Domain/MediaItem/JellyfinEpisode.cs

@ -1,5 +1,8 @@
namespace ErsatzTV.Core.Domain using System.Diagnostics;
namespace ErsatzTV.Core.Domain
{ {
[DebuggerDisplay("{EpisodeMetadata[0].Title}")]
public class JellyfinEpisode : Episode public class JellyfinEpisode : Episode
{ {
public string ItemId { get; set; } public string ItemId { get; set; }

1
ErsatzTV.Core/Domain/ProgramSchedule.cs

@ -7,6 +7,7 @@ namespace ErsatzTV.Core.Domain
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public PlaybackOrder MediaCollectionPlaybackOrder { get; set; } public PlaybackOrder MediaCollectionPlaybackOrder { get; set; }
public bool KeepMultiPartEpisodesTogether { get; set; }
public List<ProgramScheduleItem> Items { get; set; } public List<ProgramScheduleItem> Items { get; set; }
public List<Playout> Playouts { get; set; } public List<Playout> Playouts { get; set; }
} }

6
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -79,9 +79,9 @@ namespace ErsatzTV.Core.FFmpeg
IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, version); IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, version);
if (!scaledSize.IsSameSizeAs(version)) if (!scaledSize.IsSameSizeAs(version))
{ {
int fixedHeight = scaledSize.Height + (scaledSize.Height % 2); int fixedHeight = scaledSize.Height + scaledSize.Height % 2;
int fixedWidth = scaledSize.Width + (scaledSize.Width % 2); int fixedWidth = scaledSize.Width + scaledSize.Width % 2;
result.ScaledSize = Some((IDisplaySize)new DisplaySize(fixedWidth, fixedHeight)); result.ScaledSize = Some((IDisplaySize) new DisplaySize(fixedWidth, fixedHeight));
} }
} }

85
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
@ -16,7 +15,7 @@ namespace ErsatzTV.Core.Scheduling
IEnumerable<MediaItem> mediaItems, IEnumerable<MediaItem> mediaItems,
CollectionEnumeratorState state) CollectionEnumeratorState state)
{ {
_sortedMediaItems = mediaItems.OrderBy(identity, new ChronologicalComparer()).ToList(); _sortedMediaItems = mediaItems.OrderBy(identity, new ChronologicalMediaComparer()).ToList();
State = new CollectionEnumeratorState { Seed = state.Seed }; State = new CollectionEnumeratorState { Seed = state.Seed };
while (State.Index < state.Index) while (State.Index < state.Index)
@ -30,85 +29,5 @@ namespace ErsatzTV.Core.Scheduling
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
private class ChronologicalComparer : IComparer<MediaItem>
{
public int Compare(MediaItem x, MediaItem y)
{
if (x == null || y == null)
{
return 0;
}
DateTime date1 = x switch
{
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
em => em.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
Movie m => m.MovieMetadata.HeadOrNone().Match(
mm => mm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
mvm => mvm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
_ => DateTime.MaxValue
};
DateTime date2 = y switch
{
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
em => em.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
Movie m => m.MovieMetadata.HeadOrNone().Match(
mm => mm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
mvm => mvm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
_ => DateTime.MaxValue
};
if (date1 != date2)
{
return date1.CompareTo(date2);
}
int season1 = x switch
{
Episode e => e.Season?.SeasonNumber ?? int.MaxValue,
_ => int.MaxValue
};
int season2 = y switch
{
Episode e => e.Season?.SeasonNumber ?? int.MaxValue,
_ => int.MaxValue
};
if (season1 != season2)
{
return season1.CompareTo(season2);
}
int episode1 = x switch
{
Episode e => e.EpisodeNumber,
_ => int.MaxValue
};
int episode2 = y switch
{
Episode e => e.EpisodeNumber,
_ => int.MaxValue
};
if (episode1 != episode2)
{
return episode1.CompareTo(episode2);
}
return x.Id.CompareTo(y.Id);
}
}
} }
} }

86
ErsatzTV.Core/Scheduling/ChronologicalMediaComparer.cs

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling
{
internal class ChronologicalMediaComparer : IComparer<MediaItem>
{
public int Compare(MediaItem x, MediaItem y)
{
if (x == null || y == null)
{
return 0;
}
DateTime date1 = x switch
{
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
em => em.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
Movie m => m.MovieMetadata.HeadOrNone().Match(
mm => mm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
mvm => mvm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
_ => DateTime.MaxValue
};
DateTime date2 = y switch
{
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
em => em.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
Movie m => m.MovieMetadata.HeadOrNone().Match(
mm => mm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
mvm => mvm.ReleaseDate ?? DateTime.MaxValue,
() => DateTime.MaxValue),
_ => DateTime.MaxValue
};
if (date1 != date2)
{
return date1.CompareTo(date2);
}
int season1 = x switch
{
Episode e => e.Season?.SeasonNumber ?? int.MaxValue,
_ => int.MaxValue
};
int season2 = y switch
{
Episode e => e.Season?.SeasonNumber ?? int.MaxValue,
_ => int.MaxValue
};
if (season1 != season2)
{
return season1.CompareTo(season2);
}
int episode1 = x switch
{
Episode e => e.EpisodeNumber,
_ => int.MaxValue
};
int episode2 = y switch
{
Episode e => e.EpisodeNumber,
_ => int.MaxValue
};
if (episode1 != episode2)
{
return episode1.CompareTo(episode2);
}
return x.Id.CompareTo(y.Id);
}
}
}

7
ErsatzTV.Core/Scheduling/GroupedMediaItem.cs

@ -0,0 +1,7 @@
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling
{
public record GroupedMediaItem(MediaItem First, List<MediaItem> Additional);
}

87
ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
public static class MultiPartEpisodeGrouper
{
public static List<GroupedMediaItem> GroupMediaItems(IList<MediaItem> mediaItems)
{
var sortedMediaItems = mediaItems.OrderBy(identity, new ChronologicalMediaComparer()).ToList();
var groups = new List<GroupedMediaItem>();
GroupedMediaItem group = null;
var lastNumber = 0;
foreach (MediaItem item in sortedMediaItems)
{
if (item is Episode e)
{
const string PATTERN = @"^.*\((\d+)\)$";
Match match = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN);
if (match.Success)
{
var number = int.Parse(match.Groups[1].Value);
if (number == lastNumber + 1)
{
if (lastNumber == 0)
{
// start a new group
group = new GroupedMediaItem(item, null);
}
else if (group != null)
{
// add to current group
List<MediaItem> additional = group.Additional ?? new List<MediaItem>();
additional.Add(item);
group = group with { Additional = additional };
}
else
{
// this should never happen
throw new InvalidOperationException("Bad shuffle state");
}
lastNumber = number;
}
}
else
{
if (group != null && lastNumber != 0)
{
groups.Add(group);
group = null;
lastNumber = 0;
}
groups.Add(new GroupedMediaItem(item, null));
}
}
else
{
groups.Add(new GroupedMediaItem(item, null));
}
}
return groups;
}
public static IList<MediaItem> FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount)
{
var result = new MediaItem[mediaItemCount];
var i = 0;
foreach (GroupedMediaItem group in copy)
{
result[i++] = group.First;
foreach (MediaItem additional in Optional(group.Additional).Flatten())
{
result[i++] = additional;
}
}
return result;
}
}
}

5
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -468,7 +468,10 @@ namespace ErsatzTV.Core.Scheduling
case PlaybackOrder.Random: case PlaybackOrder.Random:
return new RandomizedMediaCollectionEnumerator(mediaItems, state); return new RandomizedMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.Shuffle: case PlaybackOrder.Shuffle:
return new ShuffledMediaCollectionEnumerator(mediaItems, state); return new ShuffledMediaCollectionEnumerator(
mediaItems,
state,
playout.ProgramSchedule.KeepMultiPartEpisodesTogether);
default: default:
// TODO: handle this error case differently? // TODO: handle this error case differently?
return new RandomizedMediaCollectionEnumerator(mediaItems, state); return new RandomizedMediaCollectionEnumerator(mediaItems, state);

26
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -10,14 +10,22 @@ namespace ErsatzTV.Core.Scheduling
{ {
public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
{ {
private readonly IList<MediaItem> _mediaItems; private readonly int _mediaItemCount;
private readonly IList<GroupedMediaItem> _mediaItems;
private Random _random; private Random _random;
private IList<MediaItem> _shuffled; private IList<MediaItem> _shuffled;
public ShuffledMediaCollectionEnumerator(
public ShuffledMediaCollectionEnumerator(IList<MediaItem> mediaItems, CollectionEnumeratorState state) IList<MediaItem> mediaItems,
CollectionEnumeratorState state,
bool keepMultiPartEpisodesTogether)
{ {
_mediaItems = mediaItems; _mediaItemCount = mediaItems.Count;
_mediaItems = keepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(mediaItems)
: mediaItems.Map(mi => new GroupedMediaItem(mi, null)).ToList();
_random = new Random(state.Seed); _random = new Random(state.Seed);
_shuffled = Shuffle(_mediaItems, _random); _shuffled = Shuffle(_mediaItems, _random);
@ -30,7 +38,7 @@ namespace ErsatzTV.Core.Scheduling
public CollectionEnumeratorState State { get; } public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItems.Count] : None; public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
public void MoveNext() public void MoveNext()
{ {
@ -54,21 +62,21 @@ namespace ErsatzTV.Core.Scheduling
State.Index %= _shuffled.Count; State.Index %= _shuffled.Count;
} }
private static IList<T> Shuffle<T>(IEnumerable<T> list, Random random) private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, Random random)
{ {
T[] copy = list.ToArray(); GroupedMediaItem[] copy = list.ToArray();
int n = copy.Length; int n = copy.Length;
while (n > 1) while (n > 1)
{ {
n--; n--;
int k = random.Next(n + 1); int k = random.Next(n + 1);
T value = copy[k]; GroupedMediaItem value = copy[k];
copy[k] = copy[n]; copy[k] = copy[n];
copy[n] = value; copy[n] = value;
} }
return copy; return MultiPartEpisodeGrouper.FlattenGroups(copy, _mediaItemCount);
} }
} }
} }

2474
ErsatzTV.Infrastructure/Migrations/20210517232825_Add_ProgramSchedule_KeepMultiPartEpisodesTogether.Designer.cs generated

File diff suppressed because it is too large Load Diff

20
ErsatzTV.Infrastructure/Migrations/20210517232825_Add_ProgramSchedule_KeepMultiPartEpisodesTogether.cs

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_ProgramSchedule_KeepMultiPartEpisodesTogether : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.AddColumn<bool>(
"KeepMultiPartEpisodesTogether",
"ProgramSchedule",
"INTEGER",
nullable: false,
defaultValue: false);
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropColumn(
"KeepMultiPartEpisodesTogether",
"ProgramSchedule");
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -969,6 +969,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("KeepMultiPartEpisodesTogether")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionPlaybackOrder") b.Property<int>("MediaCollectionPlaybackOrder")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

7
ErsatzTV/Pages/ScheduleEditor.razor

@ -22,6 +22,12 @@
<MudSelectItem Value="@playbackOrder">@playbackOrder</MudSelectItem> <MudSelectItem Value="@playbackOrder">@playbackOrder</MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Label="Keep Multi-Part Episodes Together"
@bind-Checked="@_model.KeepMultiPartEpisodesTogether"
Disabled="@(_model.MediaCollectionPlaybackOrder != PlaybackOrder.Shuffle)"
For="@(() => _model.KeepMultiPartEpisodesTogether)"/>
</MudElement>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary"> <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
@ -53,6 +59,7 @@
_model.Id = viewModel.Id; _model.Id = viewModel.Id;
_model.Name = viewModel.Name; _model.Name = viewModel.Name;
_model.MediaCollectionPlaybackOrder = viewModel.MediaCollectionPlaybackOrder; _model.MediaCollectionPlaybackOrder = viewModel.MediaCollectionPlaybackOrder;
_model.KeepMultiPartEpisodesTogether = viewModel.KeepMultiPartEpisodesTogether;
}, },
() => NavigationManager.NavigateTo("404")); () => NavigationManager.NavigateTo("404"));
} }

9
ErsatzTV/ViewModels/ProgramScheduleEditViewModel.cs

@ -8,7 +8,12 @@ namespace ErsatzTV.ViewModels
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public PlaybackOrder MediaCollectionPlaybackOrder { get; set; } public PlaybackOrder MediaCollectionPlaybackOrder { get; set; }
public UpdateProgramSchedule ToUpdate() => new(Id, Name, MediaCollectionPlaybackOrder); public bool KeepMultiPartEpisodesTogether { get; set; }
public CreateProgramSchedule ToCreate() => new(Name, MediaCollectionPlaybackOrder);
public UpdateProgramSchedule ToUpdate() =>
new(Id, Name, MediaCollectionPlaybackOrder, KeepMultiPartEpisodesTogether);
public CreateProgramSchedule ToCreate() =>
new(Name, MediaCollectionPlaybackOrder, KeepMultiPartEpisodesTogether);
} }
} }

Loading…
Cancel
Save