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

This commit is contained in:
Jason Dove
2023-04-13 11:06:48 -05:00
committed by GitHub
parent b8dcd26e3a
commit ab55978732
10 changed files with 216 additions and 6 deletions
+5
View File
@@ -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/).
## [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
- Limit `HLS Direct` streams to realtime speed
- Fix `Reset Playout` button to use worker thread instead of UI thread
@@ -56,6 +56,7 @@ public abstract class ProgramScheduleItemCommandBase
case PlaybackOrder.Chronological:
case PlaybackOrder.Random:
case PlaybackOrder.MultiEpisodeShuffle:
case PlaybackOrder.SeasonEpisode:
return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'");
case PlaybackOrder.Shuffle:
case PlaybackOrder.ShuffleInOrder:
@@ -78,7 +78,8 @@ public class ChronologicalContentTests
{
new()
{
ReleaseDate = new DateTime(2020, 1, i)
ReleaseDate = new DateTime(2020, 1, i),
EpisodeNumber = 20 - i
}
}
})
@@ -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();
}
+2 -1
View File
@@ -6,5 +6,6 @@ public enum PlaybackOrder
Random = 2,
Shuffle = 3,
ShuffleInOrder = 4,
MultiEpisodeShuffle = 5
MultiEpisodeShuffle = 5,
SeasonEpisode = 6
}
@@ -23,10 +23,20 @@ public class MultiCollectionGroup : GroupedMediaItem
switch (collectionWithItems.PlaybackOrder)
{
case PlaybackOrder.Chronological:
{
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer())
.ToList();
First = sortedItems.Head();
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;
default:
throw new NotSupportedException(
@@ -772,6 +772,17 @@ public class PlayoutBuilder : IPlayoutBuilder
}
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:
return new RandomizedMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.ShuffleInOrder:
@@ -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;
}
@@ -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
View File
@@ -200,6 +200,7 @@
break;
case ProgramScheduleItemCollectionType.TelevisionShow:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.SeasonEpisode">Season, Episode</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.MultiEpisodeShuffle">Multi-Episode Shuffle</MudSelectItem>