Browse Source

add multi-episode group size (#2164)

pull/2165/head
Jason Dove 4 weeks ago committed by GitHub
parent
commit
6a84c564d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 102
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  3. 2
      ErsatzTV.Core/Domain/MultipleMode.cs
  4. 24
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  5. 12
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  6. 12
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  7. 31
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

1
CHANGELOG.md

@ -111,6 +111,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -111,6 +111,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Count`: same behavior as before, requires a number of media items to play and will always schedule the same number
- `Collection Size`: similar to count of zero before, will play all media items from the collection before continuing to the next schedule item
- `Playlist Item Size`: will play all media items from the current playlist item before continuing to the next schedule item
- `Multi-Episode Group Size`: will play all media items from the current multi-part episode group, or one ungrouped media item
### Fixed
- Fix QSV acceleration in docker with older Intel devices

102
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs

@ -88,6 +88,108 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase @@ -88,6 +88,108 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
playoutItems[1].CustomTitle.ShouldBe("CustomTitle");
}
[Test]
public void Should_Schedule_Multi_Part_Size_Correctly()
{
var season = new Season { ShowId = 1 };
var collectionOne = new Collection
{
Id = 1,
Name = "Episode collection",
MediaItems =
[
new Episode
{
Id = 1,
EpisodeMetadata = [new EpisodeMetadata { Title = "Episode One (1)" }],
MediaVersions = [new MediaVersion { Duration = TimeSpan.FromHours(1), Chapters = [] }],
Season = season
},
new Episode
{
Id = 2,
EpisodeMetadata = [new EpisodeMetadata { Title = "Episode Two (2)" }],
MediaVersions = [new MediaVersion { Duration = TimeSpan.FromHours(1), Chapters = [] }],
Season = season
},
new Episode
{
Id = 3,
EpisodeMetadata = [new EpisodeMetadata { Title = "Episode Three" }],
MediaVersions = [new MediaVersion { Duration = TimeSpan.FromHours(1), Chapters = [] }],
Season = season
}
]
};
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = TimeSpan.FromHours(1),
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 0,
MultipleMode = MultipleMode.MultiEpisodeGroupSize,
CustomTitle = "CustomTitle"
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var collectionItemCount = new Dictionary<CollectionKey, int>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems.Count }
}.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionItemCount, Substitute.For<ILogger>());
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
startState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.ShouldBe(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.ShouldBe(2); // one guide group here because of custom title
playoutBuilderState.DurationFinish.IsNone.ShouldBeTrue();
playoutBuilderState.InFlood.ShouldBeFalse();
playoutBuilderState.MultipleRemaining.IsNone.ShouldBeTrue();
playoutBuilderState.InDurationFiller.ShouldBeFalse();
playoutBuilderState.ScheduleItemsEnumerator.State.Index.ShouldBe(0);
enumerator.State.Index.ShouldBe(2);
playoutItems.Count.ShouldBe(2);
playoutItems[0].MediaItemId.ShouldBe(1);
playoutItems[0].StartOffset.ShouldBe(startState.CurrentTime.AddHours(1));
playoutItems[0].GuideGroup.ShouldBe(1);
playoutItems[0].FillerKind.ShouldBe(FillerKind.None);
playoutItems[0].CustomTitle.ShouldBe("CustomTitle");
playoutItems[1].MediaItemId.ShouldBe(2);
playoutItems[1].StartOffset.ShouldBe(startState.CurrentTime.AddHours(2));
playoutItems[1].GuideGroup.ShouldBe(1);
playoutItems[1].FillerKind.ShouldBe(FillerKind.None);
playoutItems[1].CustomTitle.ShouldBe("CustomTitle");
}
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
{

2
ErsatzTV.Core/Domain/MultipleMode.cs

@ -13,5 +13,5 @@ public enum MultipleMode @@ -13,5 +13,5 @@ public enum MultipleMode
// from one item (not a multi-episode) to however many multi-episodes are linked together
// is this limited to chronological and season/episode?
MultiEpisodeSize = 3
MultiEpisodeGroupSize = 3
}

24
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -8,6 +8,7 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu @@ -8,6 +8,7 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
{
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
private readonly List<MediaItem> _sortedMediaItems;
private readonly Lazy<Dictionary<int, int>> _lazyMediaItemGroupSize;
public ChronologicalMediaCollectionEnumerator(
IEnumerable<MediaItem> mediaItems,
@ -19,6 +20,8 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu @@ -19,6 +20,8 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(() =>
_sortedMediaItems.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
_lazyMediaItemGroupSize = new Lazy<Dictionary<int, int>>(CalculateMediaItemGroupSizes);
State = new CollectionEnumeratorState { Seed = state.Seed };
if (state.Index >= _sortedMediaItems.Count)
@ -33,6 +36,24 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu @@ -33,6 +36,24 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
}
}
private Dictionary<int, int> CalculateMediaItemGroupSizes()
{
var result = new Dictionary<int, int>();
List<GroupedMediaItem> groupedItems = MultiPartEpisodeGrouper.GroupMediaItems(_sortedMediaItems, false);
foreach (GroupedMediaItem group in groupedItems)
{
int size = group.Additional.Count + 1;
result[group.First.Id] = size;
foreach (MediaItem additional in group.Additional)
{
result[additional.Id] = size;
}
}
return result;
}
public void ResetState(CollectionEnumeratorState state) =>
// seed doesn't matter in chronological
State.Index = state.Index;
@ -47,4 +68,7 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu @@ -47,4 +68,7 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _sortedMediaItems.Count;
public int GroupSizeForMediaItem(MediaItem mediaItem) =>
_lazyMediaItemGroupSize.Value.GetValueOrDefault(mediaItem.Id, 1);
}

12
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -61,6 +61,18 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -61,6 +61,18 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
};
}
break;
case MultipleMode.MultiEpisodeGroupSize:
if (contentEnumerator is ChronologicalMediaCollectionEnumerator chronologicalEnumerator)
{
foreach (MediaItem current in contentEnumerator.Current)
{
nextState = nextState with
{
MultipleRemaining = chronologicalEnumerator.GroupSizeForMediaItem(current)
};
}
}
break;
}
}

12
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -31,6 +31,9 @@ @@ -31,6 +31,9 @@
case MultipleMode.PlaylistItemSize:
mode = "Playlist Item Size";
break;
case MultipleMode.MultiEpisodeGroupSize:
mode = "Multi-Episode Group Size";
break;
case MultipleMode.Count:
default:
mode += $" ({_selectedItem.MultipleCount})";
@ -110,6 +113,9 @@ @@ -110,6 +113,9 @@
case MultipleMode.PlaylistItemSize:
@:Playlist Item Size
break;
case MultipleMode.MultiEpisodeGroupSize:
@:Multi-Episode Group Size
break;
case MultipleMode.Count:
default:
@($"Multiple ({context.MultipleCount})")
@ -379,6 +385,7 @@ @@ -379,6 +385,7 @@
</div>
<MudSelect @bind-Value="@_selectedItem.MultipleMode" For="@(() => _selectedItem.MultipleMode)">
<MudSelectItem Value="MultipleMode.Count">Count</MudSelectItem>
@if (_selectedItem.CollectionType is not ProgramScheduleItemCollectionType.Playlist)
{
<MudSelectItem Value="MultipleMode.CollectionSize">Collection Size</MudSelectItem>
@ -387,6 +394,11 @@ @@ -387,6 +394,11 @@
{
<MudSelectItem Value="MultipleMode.PlaylistItemSize">Playlist Item Size</MudSelectItem>
}
@if (_selectedItem.PlaybackOrder is PlaybackOrder.Chronological)
{
<MudSelectItem Value="MultipleMode.MultiEpisodeGroupSize">Multi-Episode Group Size</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">

31
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -20,6 +20,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -20,6 +20,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
private int _playoutDurationHours;
private int _playoutDurationMinutes;
private TimeSpan? _startTime;
private PlaybackOrder _playbackOrder;
public int Id { get; set; }
public int Index { get; set; }
@ -61,10 +62,17 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -61,10 +62,17 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
MediaItem = null;
SmartCollection = null;
if (_collectionType != ProgramScheduleItemCollectionType.Playlist &&
MultipleMode is MultipleMode.PlaylistItemSize)
{
MultipleMode = MultipleMode.Count;
}
OnPropertyChanged(nameof(Collection));
OnPropertyChanged(nameof(MultiCollection));
OnPropertyChanged(nameof(MediaItem));
OnPropertyChanged(nameof(SmartCollection));
OnPropertyChanged(nameof(MultiCollection));
}
if (_collectionType == ProgramScheduleItemCollectionType.MultiCollection)
@ -102,7 +110,28 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -102,7 +110,28 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
_ => string.Empty
};
public PlaybackOrder PlaybackOrder { get; set; }
public PlaybackOrder PlaybackOrder
{
get => _playbackOrder;
set
{
if (value == _playbackOrder)
{
return;
}
_playbackOrder = value;
if (_playbackOrder is not PlaybackOrder.Chronological && MultipleMode is MultipleMode.MultiEpisodeGroupSize)
{
MultipleMode = MultipleMode.Count;
}
OnPropertyChanged();
OnPropertyChanged(nameof(CanFillWithGroups));
OnPropertyChanged(nameof(MultipleMode));
}
}
public MultipleMode MultipleMode { get; set; }

Loading…
Cancel
Save