Browse Source

add season, episode playback order for shows (#1243)

pull/1245/head
Jason Dove 2 years ago committed by GitHub
parent
commit
ab55978732
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  3. 3
      ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs
  4. 88
      ErsatzTV.Core.Tests/Scheduling/SeasonEpisodeContentTests.cs
  5. 3
      ErsatzTV.Core/Domain/PlaybackOrder.cs
  6. 10
      ErsatzTV.Core/Scheduling/MultiCollectionGroup.cs
  7. 11
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  8. 38
      ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs
  9. 54
      ErsatzTV.Core/Scheduling/SeasonEpisodeMediaComparer.cs
  10. 1
      ErsatzTV/Pages/ScheduleItemsEditor.razor

5
CHANGELOG.md

@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- Add `Season, Episode` playback order
- This is currently *only* available when a show is added directly to a schedule
- This will ignore release date and sort exclusively by season number and then by episode number
### Fixed ### Fixed
- Limit `HLS Direct` streams to realtime speed - Limit `HLS Direct` streams to realtime speed
- Fix `Reset Playout` button to use worker thread instead of UI thread - Fix `Reset Playout` button to use worker thread instead of UI thread

1
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -56,6 +56,7 @@ public abstract class ProgramScheduleItemCommandBase
case PlaybackOrder.Chronological: case PlaybackOrder.Chronological:
case PlaybackOrder.Random: case PlaybackOrder.Random:
case PlaybackOrder.MultiEpisodeShuffle: case PlaybackOrder.MultiEpisodeShuffle:
case PlaybackOrder.SeasonEpisode:
return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'"); return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'");
case PlaybackOrder.Shuffle: case PlaybackOrder.Shuffle:
case PlaybackOrder.ShuffleInOrder: case PlaybackOrder.ShuffleInOrder:

3
ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs

@ -78,7 +78,8 @@ public class ChronologicalContentTests
{ {
new() new()
{ {
ReleaseDate = new DateTime(2020, 1, i) ReleaseDate = new DateTime(2020, 1, i),
EpisodeNumber = 20 - i
} }
} }
}) })

88
ErsatzTV.Core.Tests/Scheduling/SeasonEpisodeContentTests.cs

@ -0,0 +1,88 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using FluentAssertions;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class SeasonEpisodeContentTests
{
[Test]
public void Episodes_Should_Sort_By_EpisodeNumber()
{
List<MediaItem> contents = Episodes(10);
var state = new CollectionEnumeratorState();
var chronologicalContent = new SeasonEpisodeMediaCollectionEnumerator(contents, state);
for (var i = 1; i <= 10; i++)
{
chronologicalContent.Current.IsSome.Should().BeTrue();
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
chronologicalContent.MoveNext();
}
}
[Test]
public void State_Index_Should_Increment()
{
List<MediaItem> contents = Episodes(10);
var state = new CollectionEnumeratorState();
var chronologicalContent = new SeasonEpisodeMediaCollectionEnumerator(contents, state);
for (var i = 0; i < 10; i++)
{
chronologicalContent.State.Index.Should().Be(i % 10);
chronologicalContent.MoveNext();
}
}
[Test]
public void State_Should_Impact_Iterator_Start()
{
List<MediaItem> contents = Episodes(10);
var state = new CollectionEnumeratorState { Index = 5 };
var chronologicalContent = new SeasonEpisodeMediaCollectionEnumerator(contents, state);
for (var i = 6; i <= 10; i++)
{
chronologicalContent.Current.IsSome.Should().BeTrue();
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
chronologicalContent.State.Index.Should().Be(i - 1);
chronologicalContent.MoveNext();
}
}
[Test]
[Timeout(1000)]
public void State_Should_Reset_When_Invalid()
{
List<MediaItem> contents = Episodes(10);
var state = new CollectionEnumeratorState { Index = 10 };
var chronologicalContent = new SeasonEpisodeMediaCollectionEnumerator(contents, state);
chronologicalContent.State.Index.Should().Be(0);
chronologicalContent.State.Seed.Should().Be(0);
}
private static List<MediaItem> Episodes(int count) =>
Range(1, count).Map(
i => (MediaItem)new Episode
{
Id = i,
EpisodeMetadata = new List<EpisodeMetadata>
{
new()
{
ReleaseDate = new DateTime(2020, 1, 20 - i),
EpisodeNumber = i
}
}
})
.Reverse()
.ToList();
}

3
ErsatzTV.Core/Domain/PlaybackOrder.cs

@ -6,5 +6,6 @@ public enum PlaybackOrder
Random = 2, Random = 2,
Shuffle = 3, Shuffle = 3,
ShuffleInOrder = 4, ShuffleInOrder = 4,
MultiEpisodeShuffle = 5 MultiEpisodeShuffle = 5,
SeasonEpisode = 6
} }

10
ErsatzTV.Core/Scheduling/MultiCollectionGroup.cs

@ -23,10 +23,20 @@ public class MultiCollectionGroup : GroupedMediaItem
switch (collectionWithItems.PlaybackOrder) switch (collectionWithItems.PlaybackOrder)
{ {
case PlaybackOrder.Chronological: case PlaybackOrder.Chronological:
{
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer()) var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer())
.ToList(); .ToList();
First = sortedItems.Head(); First = sortedItems.Head();
Additional = sortedItems.Tail().ToList(); Additional = sortedItems.Tail().ToList();
}
break;
case PlaybackOrder.SeasonEpisode:
{
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new SeasonEpisodeMediaComparer())
.ToList();
First = sortedItems.Head();
Additional = sortedItems.Tail().ToList();
}
break; break;
default: default:
throw new NotSupportedException( throw new NotSupportedException(

11
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -772,6 +772,17 @@ public class PlayoutBuilder : IPlayoutBuilder
} }
return new ChronologicalMediaCollectionEnumerator(mediaItems, state); return new ChronologicalMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.SeasonEpisode:
if (randomStartPoint)
{
state = new CollectionEnumeratorState
{
Seed = state.Seed,
Index = Random.Next(0, mediaItems.Count - 1)
};
}
return new SeasonEpisodeMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.Random: case PlaybackOrder.Random:
return new RandomizedMediaCollectionEnumerator(mediaItems, state); return new RandomizedMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.ShuffleInOrder: case PlaybackOrder.ShuffleInOrder:

38
ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs

@ -0,0 +1,38 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling;
public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnumerator
{
private readonly IList<MediaItem> _sortedMediaItems;
public SeasonEpisodeMediaCollectionEnumerator(
IEnumerable<MediaItem> mediaItems,
CollectionEnumeratorState state)
{
_sortedMediaItems = mediaItems.OrderBy(identity, new SeasonEpisodeMediaComparer()).ToList();
State = new CollectionEnumeratorState { Seed = state.Seed };
if (state.Index >= _sortedMediaItems.Count)
{
state.Index = 0;
state.Seed = 0;
}
while (State.Index < state.Index)
{
MoveNext();
}
}
public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<MediaItem> Peek(int offset) =>
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
}

54
ErsatzTV.Core/Scheduling/SeasonEpisodeMediaComparer.cs

@ -0,0 +1,54 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling;
internal class SeasonEpisodeMediaComparer : IComparer<MediaItem>
{
public int Compare(MediaItem x, MediaItem y)
{
if (x == null || y == null)
{
return 0;
}
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.EpisodeMetadata.HeadOrNone().Match(
em => em.EpisodeNumber,
() => int.MaxValue),
_ => int.MaxValue
};
int episode2 = y switch
{
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
em => em.EpisodeNumber,
() => int.MaxValue),
_ => int.MaxValue
};
if (episode1 != episode2)
{
return episode1.CompareTo(episode2);
}
return x.Id.CompareTo(y.Id);
}
}

1
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -200,6 +200,7 @@
break; break;
case ProgramScheduleItemCollectionType.TelevisionShow: case ProgramScheduleItemCollectionType.TelevisionShow:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.SeasonEpisode">Season, Episode</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.MultiEpisodeShuffle">Multi-Episode Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.MultiEpisodeShuffle">Multi-Episode Shuffle</MudSelectItem>

Loading…
Cancel
Save