add season, episode playback order for shows (#1243)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user